ominfra 0.0.0.dev174__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/conf.py +12 -6
- ominfra/manage/deploy/paths/paths.py +2 -2
- ominfra/manage/deploy/specs.py +3 -9
- 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 +1429 -1373
- ominfra/scripts/supervisor.py +40 -5
- {ominfra-0.0.0.dev174.dist-info → ominfra-0.0.0.dev176.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev174.dist-info → ominfra-0.0.0.dev176.dist-info}/RECORD +18 -18
- {ominfra-0.0.0.dev174.dist-info → ominfra-0.0.0.dev176.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev174.dist-info → ominfra-0.0.0.dev176.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev174.dist-info → ominfra-0.0.0.dev176.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev174.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,361 +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.
|
6976
|
-
|
6977
|
-
|
6978
|
-
|
6979
|
-
return DEPLOY_TAGS_BY_NAME[self.name]
|
6980
|
-
|
6981
|
-
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
6982
|
-
if tags is not None:
|
6983
|
-
return tags[self.tag].s
|
6984
|
-
else:
|
6985
|
-
return DEPLOY_TAG_SIGIL + self.name
|
6986
|
-
|
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)
|
6987
6776
|
|
6988
|
-
|
6989
|
-
class DelimiterDeployPathNamePart(DeployPathNamePart):
|
6990
|
-
delimiter: str
|
6777
|
+
#
|
6991
6778
|
|
6992
|
-
|
6993
|
-
|
6779
|
+
tag_name: ta.ClassVar[str]
|
6780
|
+
tag_kwarg: ta.ClassVar[str]
|
6994
6781
|
|
6995
|
-
def
|
6996
|
-
|
6782
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
6783
|
+
super().__init_subclass__(**kwargs)
|
6997
6784
|
|
6785
|
+
if abc.ABC in cls.__bases__:
|
6786
|
+
return
|
6998
6787
|
|
6999
|
-
|
7000
|
-
|
7001
|
-
|
6788
|
+
for b in cls.__bases__:
|
6789
|
+
if issubclass(b, DeployTag):
|
6790
|
+
check.in_(abc.ABC, b.__bases__)
|
7002
6791
|
|
7003
|
-
|
7004
|
-
check.
|
7005
|
-
|
7006
|
-
check.not_in(c, self.const)
|
6792
|
+
check.non_empty_str(tn := cls.tag_name)
|
6793
|
+
check.equal(tn, tn.lower().strip())
|
6794
|
+
check.not_in('_', tn)
|
7007
6795
|
|
7008
|
-
|
7009
|
-
|
6796
|
+
check.state(not hasattr(cls, 'tag_kwarg'))
|
6797
|
+
cls.tag_kwarg = tn.replace('-', '_')
|
7010
6798
|
|
7011
6799
|
|
7012
|
-
|
7013
|
-
class DeployPathName(DeployPathRenderable):
|
7014
|
-
parts: ta.Sequence[DeployPathNamePart]
|
6800
|
+
##
|
7015
6801
|
|
7016
|
-
def __post_init__(self) -> None:
|
7017
|
-
hash(self)
|
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}')
|
7022
6802
|
|
7023
|
-
|
7024
|
-
|
6803
|
+
_DEPLOY_TAGS: ta.Set[ta.Type[DeployTag]] = set()
|
6804
|
+
DEPLOY_TAGS: ta.AbstractSet[ta.Type[DeployTag]] = _DEPLOY_TAGS
|
7025
6805
|
|
7026
|
-
|
7027
|
-
|
7028
|
-
check.non_empty_str(s)
|
7029
|
-
check.not_in('/', s)
|
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
|
7030
6808
|
|
7031
|
-
|
7032
|
-
|
7033
|
-
while i < len(s):
|
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)
|
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
|
7042
6811
|
|
7043
|
-
return cls(tuple(DeployPathNamePart.parse(p) for p in ps))
|
7044
6812
|
|
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)
|
7045
6816
|
|
7046
|
-
|
6817
|
+
_DEPLOY_TAGS.add(cls)
|
6818
|
+
_DEPLOY_TAGS_BY_NAME[cls.tag_name] = cls
|
6819
|
+
_DEPLOY_TAGS_BY_KWARG[cls.tag_kwarg] = cls
|
7047
6820
|
|
6821
|
+
register_type_obj_marshaler(cls, SingleFieldObjMarshaler(cls, 's'))
|
7048
6822
|
|
7049
|
-
|
7050
|
-
class DeployPathPart(DeployPathRenderable, abc.ABC): # noqa
|
7051
|
-
name: DeployPathName
|
6823
|
+
return cls
|
7052
6824
|
|
7053
|
-
@property
|
7054
|
-
@abc.abstractmethod
|
7055
|
-
def kind(self) -> DeployPathKind:
|
7056
|
-
raise NotImplementedError
|
7057
6825
|
|
7058
|
-
|
7059
|
-
return self.name.render(tags) + ('/' if self.kind == 'dir' else '')
|
6826
|
+
##
|
7060
6827
|
|
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
6828
|
|
7068
|
-
|
7069
|
-
|
7070
|
-
|
7071
|
-
else:
|
7072
|
-
return FileDeployPathPart(n)
|
6829
|
+
@_register_deploy_tag
|
6830
|
+
class DeployTime(DeployTag):
|
6831
|
+
tag_name: ta.ClassVar[str] = 'time'
|
7073
6832
|
|
7074
6833
|
|
7075
|
-
|
7076
|
-
@property
|
7077
|
-
def kind(self) -> DeployPathKind:
|
7078
|
-
return 'dir'
|
6834
|
+
##
|
7079
6835
|
|
7080
6836
|
|
7081
|
-
class
|
7082
|
-
|
7083
|
-
def kind(self) -> DeployPathKind:
|
7084
|
-
return 'file'
|
6837
|
+
class NameDeployTag(DeployTag, abc.ABC): # noqa
|
6838
|
+
pass
|
7085
6839
|
|
7086
6840
|
|
7087
|
-
|
6841
|
+
@_register_deploy_tag
|
6842
|
+
class DeployApp(NameDeployTag):
|
6843
|
+
tag_name: ta.ClassVar[str] = 'app'
|
7088
6844
|
|
7089
6845
|
|
7090
|
-
@
|
7091
|
-
class
|
7092
|
-
|
6846
|
+
@_register_deploy_tag
|
6847
|
+
class DeployConf(NameDeployTag):
|
6848
|
+
tag_name: ta.ClassVar[str] = 'conf'
|
7093
6849
|
|
7094
|
-
@property
|
7095
|
-
def name_parts(self) -> ta.Iterator[DeployPathNamePart]:
|
7096
|
-
for p in self.parts:
|
7097
|
-
yield from p.name.parts
|
7098
6850
|
|
7099
|
-
|
7100
|
-
hash(self)
|
7101
|
-
check.not_empty(self.parts)
|
7102
|
-
for p in self.parts[:-1]:
|
7103
|
-
check.equal(p.kind, 'dir')
|
6851
|
+
##
|
7104
6852
|
|
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
6853
|
|
7113
|
-
|
7114
|
-
|
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(abc.ABC): # 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
|
-
def __post_init__(self) -> None:
|
7220
|
-
check_valid_deploy_spec_path(self.src)
|
7221
|
-
if '/' in self.src:
|
7222
|
-
check.equal(self.src.count('/'), 1)
|
7223
|
-
|
7224
|
-
|
7225
|
-
class CurrentOnlyDeployAppConfLink(DeployAppConfLink):
|
7226
|
-
pass
|
7227
|
-
|
7228
|
-
|
7229
|
-
class AllActiveDeployAppConfLink(DeployAppConfLink):
|
7230
|
-
pass
|
7231
|
-
|
7232
|
-
|
7233
|
-
#
|
7234
|
-
|
7235
|
-
|
7236
|
-
@dc.dataclass(frozen=True)
|
7237
|
-
class DeployAppConfSpec:
|
7238
|
-
files: ta.Optional[ta.Sequence[DeployAppConfFile]] = None
|
7239
|
-
|
7240
|
-
links: ta.Optional[ta.Sequence[DeployAppConfLink]] = None
|
7241
|
-
|
7242
|
-
def __post_init__(self) -> None:
|
7243
|
-
if self.files:
|
7244
|
-
seen: ta.Set[str] = set()
|
7245
|
-
for f in self.files:
|
7246
|
-
check.not_in(f.path, seen)
|
7247
|
-
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)
|
7248
6902
|
|
6903
|
+
self._dct = dct
|
6904
|
+
self._tup = tuple(sorted((type(t).tag_kwarg, t.s) for t in dct.values()))
|
7249
6905
|
|
7250
|
-
|
6906
|
+
#
|
7251
6907
|
|
6908
|
+
def add(self, *args: ta.Any, **kwargs: ta.Any) -> 'DeployTagMap':
|
6909
|
+
return DeployTagMap(
|
6910
|
+
*self,
|
6911
|
+
*args,
|
6912
|
+
**kwargs,
|
6913
|
+
)
|
7252
6914
|
|
7253
|
-
|
7254
|
-
|
7255
|
-
|
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
|
+
}
|
7256
6920
|
|
7257
|
-
|
6921
|
+
return DeployTagMap(*[
|
6922
|
+
t
|
6923
|
+
for t in self._dct.values()
|
6924
|
+
if t not in dcs
|
6925
|
+
])
|
7258
6926
|
|
7259
|
-
|
6927
|
+
#
|
7260
6928
|
|
7261
|
-
|
6929
|
+
def __repr__(self) -> str:
|
6930
|
+
return f'{self.__class__.__name__}({", ".join(f"{k}={v!r}" for k, v in self._tup)})'
|
7262
6931
|
|
7263
|
-
|
7264
|
-
|
7265
|
-
return DeployAppKey(self._key_str())
|
6932
|
+
def __hash__(self) -> int:
|
6933
|
+
return hash(self._tup)
|
7266
6934
|
|
6935
|
+
def __eq__(self, other: object) -> bool:
|
6936
|
+
if isinstance(other, DeployTagMap):
|
6937
|
+
return self._tup == other._tup
|
6938
|
+
else:
|
6939
|
+
return NotImplemented
|
7267
6940
|
|
7268
|
-
|
6941
|
+
#
|
7269
6942
|
|
6943
|
+
def __len__(self) -> int:
|
6944
|
+
return len(self._dct)
|
7270
6945
|
|
7271
|
-
|
7272
|
-
|
7273
|
-
apps: ta.Sequence[DeployAppSpec]
|
6946
|
+
def __iter__(self) -> ta.Iterator[DeployTag]:
|
6947
|
+
return iter(self._dct.values())
|
7274
6948
|
|
7275
|
-
def
|
7276
|
-
|
7277
|
-
|
7278
|
-
|
7279
|
-
|
7280
|
-
|
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)
|
7281
6956
|
|
7282
|
-
|
7283
|
-
|
7284
|
-
|
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)
|
7285
6964
|
|
7286
6965
|
|
7287
6966
|
########################################
|
@@ -7869,290 +7548,424 @@ class LocalCommandExecutor(CommandExecutor):
|
|
7869
7548
|
|
7870
7549
|
|
7871
7550
|
########################################
|
7872
|
-
# ../deploy/
|
7551
|
+
# ../deploy/paths/paths.py
|
7873
7552
|
"""
|
7874
7553
|
TODO:
|
7875
|
-
-
|
7876
|
-
-
|
7877
|
-
|
7878
|
-
|
7879
|
-
-
|
7880
|
-
|
7881
|
-
- 2) for each subdir not needing modification, hardlink into temp dir
|
7882
|
-
- 3) for each subdir needing modification, new subdir, hardlink all files not needing modification
|
7883
|
-
- 4) write (or if deleting, omit) new files
|
7884
|
-
- 5) swap top level
|
7885
|
-
- ** whole deploy can be atomic(-ish) - do this for everything **
|
7886
|
-
- just a '/deploy/current' dir
|
7887
|
-
- some things (venvs) cannot be moved, thus the /deploy/venvs dir
|
7888
|
-
- ** ensure (enforce) equivalent relpath nesting
|
7554
|
+
- run/{.pid,.sock}
|
7555
|
+
- logs/...
|
7556
|
+
- current symlink
|
7557
|
+
- conf/{nginx,supervisor}
|
7558
|
+
- env/?
|
7559
|
+
- apps/<app>/shared
|
7889
7560
|
"""
|
7890
7561
|
|
7891
7562
|
|
7892
|
-
|
7893
|
-
def __init__(
|
7894
|
-
self,
|
7895
|
-
*,
|
7896
|
-
deploy_home: ta.Optional[DeployHome] = None,
|
7897
|
-
) -> None:
|
7898
|
-
super().__init__()
|
7563
|
+
##
|
7899
7564
|
|
7900
|
-
self._deploy_home = deploy_home
|
7901
7565
|
|
7902
|
-
|
7566
|
+
class DeployPathError(Exception):
|
7567
|
+
pass
|
7903
7568
|
|
7904
|
-
async def _write_app_conf_file(
|
7905
|
-
self,
|
7906
|
-
acf: DeployAppConfFile,
|
7907
|
-
app_conf_dir: str,
|
7908
|
-
) -> None:
|
7909
|
-
conf_file = os.path.join(app_conf_dir, acf.path)
|
7910
|
-
check.arg(is_path_in_dir(app_conf_dir, conf_file))
|
7911
7569
|
|
7912
|
-
|
7570
|
+
class DeployPathRenderable(abc.ABC):
|
7571
|
+
@abc.abstractmethod
|
7572
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7573
|
+
raise NotImplementedError
|
7913
7574
|
|
7914
|
-
with open(conf_file, 'w') as f: # noqa
|
7915
|
-
f.write(acf.body)
|
7916
7575
|
|
7917
|
-
|
7576
|
+
##
|
7918
7577
|
|
7919
|
-
class _ComputedConfLink(ta.NamedTuple):
|
7920
|
-
is_dir: bool
|
7921
|
-
link_src: str
|
7922
|
-
link_dst: str
|
7923
7578
|
|
7924
|
-
|
7925
|
-
|
7579
|
+
class DeployPathNamePart(DeployPathRenderable, abc.ABC):
|
7580
|
+
@classmethod
|
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)
|
7926
7589
|
|
7927
|
-
def _compute_app_conf_link_dst(
|
7928
|
-
self,
|
7929
|
-
link: DeployAppConfLink,
|
7930
|
-
tags: DeployTagMap,
|
7931
|
-
app_conf_dir: str,
|
7932
|
-
conf_link_dir: str,
|
7933
|
-
) -> _ComputedConfLink:
|
7934
|
-
link_src = os.path.join(app_conf_dir, link.src)
|
7935
|
-
check.arg(is_path_in_dir(app_conf_dir, link_src))
|
7936
7590
|
|
7937
|
-
|
7591
|
+
@dc.dataclass(frozen=True)
|
7592
|
+
class TagDeployPathNamePart(DeployPathNamePart):
|
7593
|
+
name: str
|
7938
7594
|
|
7939
|
-
|
7940
|
-
|
7941
|
-
check.arg(link.src.count('/') == 1)
|
7942
|
-
link_dst_pfx = link.src
|
7943
|
-
link_dst_sfx = ''
|
7595
|
+
def __post_init__(self) -> None:
|
7596
|
+
check.in_(self.name, DEPLOY_TAGS_BY_NAME)
|
7944
7597
|
|
7945
|
-
|
7946
|
-
|
7947
|
-
|
7948
|
-
# TODO: check filename :|
|
7949
|
-
link_dst_pfx = d + '/'
|
7950
|
-
link_dst_sfx = DEPLOY_TAG_SEPARATOR + f
|
7598
|
+
@property
|
7599
|
+
def tag(self) -> ta.Type[DeployTag]:
|
7600
|
+
return DEPLOY_TAGS_BY_NAME[self.name]
|
7951
7601
|
|
7952
|
-
|
7953
|
-
|
7954
|
-
|
7955
|
-
|
7956
|
-
|
7957
|
-
link_dst_sfx = '.' + r
|
7958
|
-
else:
|
7959
|
-
link_dst_pfx = link.src + '/'
|
7960
|
-
link_dst_sfx = ''
|
7602
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7603
|
+
if tags is not None:
|
7604
|
+
return tags[self.tag].s
|
7605
|
+
else:
|
7606
|
+
return DEPLOY_TAG_SIGIL + self.name
|
7961
7607
|
|
7962
|
-
#
|
7963
7608
|
|
7964
|
-
|
7965
|
-
|
7966
|
-
|
7967
|
-
link_dst_mid = self._UNIQUE_LINK_NAME.render(tags)
|
7968
|
-
else:
|
7969
|
-
raise TypeError(link)
|
7609
|
+
@dc.dataclass(frozen=True)
|
7610
|
+
class DelimiterDeployPathNamePart(DeployPathNamePart):
|
7611
|
+
delimiter: str
|
7970
7612
|
|
7971
|
-
|
7613
|
+
def __post_init__(self) -> None:
|
7614
|
+
check.in_(self.delimiter, DEPLOY_TAG_DELIMITERS)
|
7972
7615
|
|
7973
|
-
|
7974
|
-
|
7975
|
-
link_dst_mid,
|
7976
|
-
link_dst_sfx,
|
7977
|
-
])
|
7978
|
-
link_dst = os.path.join(conf_link_dir, link_dst_name)
|
7616
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7617
|
+
return self.delimiter
|
7979
7618
|
|
7980
|
-
return DeployConfManager._ComputedConfLink(
|
7981
|
-
is_dir=is_dir,
|
7982
|
-
link_src=link_src,
|
7983
|
-
link_dst=link_dst,
|
7984
|
-
)
|
7985
7619
|
|
7986
|
-
|
7987
|
-
|
7988
|
-
|
7989
|
-
tags: DeployTagMap,
|
7990
|
-
app_conf_dir: str,
|
7991
|
-
conf_link_dir: str,
|
7992
|
-
) -> None:
|
7993
|
-
comp = self._compute_app_conf_link_dst(
|
7994
|
-
link,
|
7995
|
-
tags,
|
7996
|
-
app_conf_dir,
|
7997
|
-
conf_link_dir,
|
7998
|
-
)
|
7620
|
+
@dc.dataclass(frozen=True)
|
7621
|
+
class ConstDeployPathNamePart(DeployPathNamePart):
|
7622
|
+
const: str
|
7999
7623
|
|
8000
|
-
|
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)
|
8001
7628
|
|
8002
|
-
|
8003
|
-
|
7629
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7630
|
+
return self.const
|
8004
7631
|
|
8005
|
-
if comp.is_dir:
|
8006
|
-
check.arg(os.path.isdir(comp.link_src))
|
8007
|
-
else:
|
8008
|
-
check.arg(os.path.isfile(comp.link_src))
|
8009
7632
|
|
8010
|
-
|
7633
|
+
@dc.dataclass(frozen=True)
|
7634
|
+
class DeployPathName(DeployPathRenderable):
|
7635
|
+
parts: ta.Sequence[DeployPathNamePart]
|
8011
7636
|
|
8012
|
-
|
8013
|
-
|
8014
|
-
|
8015
|
-
|
8016
|
-
|
8017
|
-
|
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}')
|
8018
7643
|
|
8019
|
-
|
7644
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7645
|
+
return ''.join(p.render(tags) for p in self.parts)
|
8020
7646
|
|
8021
|
-
|
8022
|
-
|
8023
|
-
|
8024
|
-
|
8025
|
-
app_conf_dir: str,
|
8026
|
-
conf_link_dir: str,
|
8027
|
-
) -> None:
|
8028
|
-
for acf in spec.files or []:
|
8029
|
-
await self._write_app_conf_file(
|
8030
|
-
acf,
|
8031
|
-
app_conf_dir,
|
8032
|
-
)
|
7647
|
+
@classmethod
|
7648
|
+
def parse(cls, s: str) -> 'DeployPathName':
|
7649
|
+
check.non_empty_str(s)
|
7650
|
+
check.not_in('/', s)
|
8033
7651
|
|
8034
|
-
|
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)
|
8035
7663
|
|
8036
|
-
for
|
8037
|
-
await self._make_app_conf_link(
|
8038
|
-
link,
|
8039
|
-
tags,
|
8040
|
-
app_conf_dir,
|
8041
|
-
conf_link_dir,
|
8042
|
-
)
|
7664
|
+
return cls(tuple(DeployPathNamePart.parse(p) for p in ps))
|
8043
7665
|
|
8044
7666
|
|
8045
|
-
|
8046
|
-
# ../deploy/paths/owners.py
|
7667
|
+
##
|
8047
7668
|
|
8048
7669
|
|
8049
|
-
|
7670
|
+
@dc.dataclass(frozen=True)
|
7671
|
+
class DeployPathPart(DeployPathRenderable, abc.ABC): # noqa
|
7672
|
+
name: DeployPathName
|
7673
|
+
|
7674
|
+
@property
|
8050
7675
|
@abc.abstractmethod
|
8051
|
-
def
|
7676
|
+
def kind(self) -> DeployPathKind:
|
8052
7677
|
raise NotImplementedError
|
8053
7678
|
|
7679
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7680
|
+
return self.name.render(tags) + ('/' if self.kind == 'dir' else '')
|
8054
7681
|
|
8055
|
-
|
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)
|
8056
7688
|
|
7689
|
+
n = DeployPathName.parse(s)
|
7690
|
+
if is_dir:
|
7691
|
+
return DirDeployPathPart(n)
|
7692
|
+
else:
|
7693
|
+
return FileDeployPathPart(n)
|
8057
7694
|
|
8058
|
-
class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
|
8059
|
-
def __init__(
|
8060
|
-
self,
|
8061
|
-
*args: ta.Any,
|
8062
|
-
owned_dir: str,
|
8063
|
-
deploy_home: ta.Optional[DeployHome],
|
8064
|
-
**kwargs: ta.Any,
|
8065
|
-
) -> None:
|
8066
|
-
super().__init__(*args, **kwargs)
|
8067
7695
|
|
8068
|
-
|
8069
|
-
|
7696
|
+
class DirDeployPathPart(DeployPathPart):
|
7697
|
+
@property
|
7698
|
+
def kind(self) -> DeployPathKind:
|
7699
|
+
return 'dir'
|
8070
7700
|
|
8071
|
-
self._deploy_home = deploy_home
|
8072
7701
|
|
8073
|
-
|
7702
|
+
class FileDeployPathPart(DeployPathPart):
|
7703
|
+
@property
|
7704
|
+
def kind(self) -> DeployPathKind:
|
7705
|
+
return 'file'
|
8074
7706
|
|
8075
|
-
@cached_nullary
|
8076
|
-
def _dir(self) -> str:
|
8077
|
-
return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
|
8078
7707
|
|
8079
|
-
|
8080
|
-
def _make_dir(self) -> str:
|
8081
|
-
if not os.path.isdir(d := self._dir()):
|
8082
|
-
os.makedirs(d, exist_ok=True)
|
8083
|
-
return d
|
7708
|
+
##
|
8084
7709
|
|
8085
|
-
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
8086
|
-
return self._owned_deploy_paths
|
8087
7710
|
|
7711
|
+
@dc.dataclass(frozen=True)
|
7712
|
+
class DeployPath:
|
7713
|
+
parts: ta.Sequence[DeployPathPart]
|
8088
7714
|
|
8089
|
-
|
8090
|
-
|
8091
|
-
|
8092
|
-
|
8093
|
-
- sequence all messages
|
8094
|
-
"""
|
7715
|
+
@property
|
7716
|
+
def name_parts(self) -> ta.Iterator[DeployPathNamePart]:
|
7717
|
+
for p in self.parts:
|
7718
|
+
yield from p.name.parts
|
8095
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')
|
8096
7725
|
|
8097
|
-
|
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
|
8098
7733
|
|
7734
|
+
@property
|
7735
|
+
def kind(self) -> DeployPathKind:
|
7736
|
+
return self.parts[-1].kind
|
8099
7737
|
|
8100
|
-
|
8101
|
-
|
8102
|
-
async def send(self, chan: RemoteChannel) -> None:
|
8103
|
-
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])
|
8104
7740
|
|
8105
|
-
|
8106
|
-
|
8107
|
-
|
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))
|
8108
7746
|
|
8109
|
-
#
|
8110
7747
|
|
8111
|
-
|
8112
|
-
|
7748
|
+
########################################
|
7749
|
+
# ../deploy/specs.py
|
8113
7750
|
|
8114
|
-
@dc.dataclass(frozen=True)
|
8115
|
-
class CommandRequest(Request):
|
8116
|
-
seq: int
|
8117
|
-
cmd: Command
|
8118
7751
|
|
8119
|
-
|
8120
|
-
class PingRequest(Request):
|
8121
|
-
time: float
|
7752
|
+
##
|
8122
7753
|
|
8123
|
-
#
|
8124
7754
|
|
8125
|
-
|
8126
|
-
|
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
|
8127
7761
|
|
8128
|
-
@dc.dataclass(frozen=True)
|
8129
|
-
class LogResponse(Response):
|
8130
|
-
s: str
|
8131
7762
|
|
8132
|
-
|
8133
|
-
|
8134
|
-
|
8135
|
-
|
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]
|
8136
7767
|
|
8137
|
-
@
|
8138
|
-
|
8139
|
-
|
7768
|
+
@abc.abstractmethod
|
7769
|
+
def key(self) -> KeyDeployTagT:
|
7770
|
+
raise NotImplementedError
|
8140
7771
|
|
8141
7772
|
|
8142
7773
|
##
|
8143
7774
|
|
8144
7775
|
|
8145
|
-
|
8146
|
-
|
8147
|
-
|
8148
|
-
|
8149
|
-
|
8150
|
-
|
8151
|
-
|
8152
|
-
|
8153
|
-
self.
|
8154
|
-
|
8155
|
-
|
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,
|
7963
|
+
) -> None:
|
7964
|
+
super().__init__()
|
7965
|
+
|
7966
|
+
self._chan = chan
|
7967
|
+
self._loop = loop
|
7968
|
+
|
8156
7969
|
def emit(self, record):
|
8157
7970
|
msg = self.format(record)
|
8158
7971
|
|
@@ -8873,266 +8686,258 @@ class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCom
|
|
8873
8686
|
|
8874
8687
|
|
8875
8688
|
########################################
|
8876
|
-
# ../deploy/
|
8689
|
+
# ../deploy/conf.py
|
8877
8690
|
"""
|
8878
8691
|
TODO:
|
8879
|
-
-
|
8880
|
-
|
8881
|
-
|
8882
|
-
|
8883
|
-
|
8884
|
-
|
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
|
8885
8706
|
"""
|
8886
8707
|
|
8887
8708
|
|
8888
|
-
|
8889
|
-
|
8890
|
-
|
8891
|
-
class DeployGitManager(SingleDirDeployPathOwner):
|
8709
|
+
class DeployConfManager:
|
8892
8710
|
def __init__(
|
8893
8711
|
self,
|
8894
8712
|
*,
|
8895
8713
|
deploy_home: ta.Optional[DeployHome] = None,
|
8896
|
-
atomics: AtomicPathSwapping,
|
8897
8714
|
) -> None:
|
8898
|
-
super().__init__(
|
8899
|
-
owned_dir='git',
|
8900
|
-
deploy_home=deploy_home,
|
8901
|
-
)
|
8715
|
+
super().__init__()
|
8902
8716
|
|
8903
|
-
self.
|
8717
|
+
self._deploy_home = deploy_home
|
8904
8718
|
|
8905
|
-
|
8719
|
+
#
|
8906
8720
|
|
8907
|
-
|
8908
|
-
|
8909
|
-
|
8910
|
-
|
8911
|
-
|
8912
|
-
|
8913
|
-
|
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))
|
8914
8728
|
|
8915
|
-
|
8916
|
-
self._repo = repo
|
8917
|
-
self._dir = os.path.join(
|
8918
|
-
self._git._make_dir(), # noqa
|
8919
|
-
check.non_empty_str(repo.host),
|
8920
|
-
check.non_empty_str(repo.path),
|
8921
|
-
)
|
8729
|
+
os.makedirs(os.path.dirname(conf_file), exist_ok=True)
|
8922
8730
|
|
8923
|
-
|
8924
|
-
|
8925
|
-
return self._repo
|
8731
|
+
with open(conf_file, 'w') as f: # noqa
|
8732
|
+
f.write(acf.body)
|
8926
8733
|
|
8927
|
-
|
8928
|
-
|
8929
|
-
|
8930
|
-
|
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))
|
8755
|
+
|
8756
|
+
#
|
8757
|
+
|
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
|
8931
8780
|
else:
|
8932
|
-
|
8781
|
+
conf = DeployConf(link.src)
|
8782
|
+
link_dst_pfx = link.src + '/'
|
8783
|
+
link_dst_sfx = ''
|
8933
8784
|
|
8934
8785
|
#
|
8935
8786
|
|
8936
|
-
|
8937
|
-
|
8938
|
-
|
8939
|
-
|
8940
|
-
|
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)
|
8941
8793
|
|
8942
8794
|
#
|
8943
8795
|
|
8944
|
-
|
8945
|
-
|
8946
|
-
|
8947
|
-
|
8948
|
-
|
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)
|
8949
8802
|
|
8950
|
-
|
8951
|
-
|
8803
|
+
return DeployConfManager._ComputedConfLink(
|
8804
|
+
conf=conf,
|
8805
|
+
is_dir=is_dir,
|
8806
|
+
link_src=link_src,
|
8807
|
+
link_dst=link_dst,
|
8808
|
+
)
|
8952
8809
|
|
8953
|
-
|
8954
|
-
|
8955
|
-
|
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
|
+
)
|
8956
8823
|
|
8957
8824
|
#
|
8958
8825
|
|
8959
|
-
|
8960
|
-
|
8961
|
-
with self._git._atomics.begin_atomic_path_swap( # noqa
|
8962
|
-
'dir',
|
8963
|
-
dst_dir,
|
8964
|
-
auto_commit=True,
|
8965
|
-
make_dirs=True,
|
8966
|
-
) as dst_swap:
|
8967
|
-
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))
|
8968
8828
|
|
8969
|
-
|
8970
|
-
|
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))
|
8971
8833
|
|
8972
|
-
|
8973
|
-
await dst_call('git', 'fetch', '--depth=1', 'local', spec.rev)
|
8974
|
-
await dst_call('git', 'checkout', spec.rev, *(spec.subtrees or []))
|
8834
|
+
#
|
8975
8835
|
|
8976
|
-
|
8977
|
-
|
8978
|
-
|
8979
|
-
|
8980
|
-
|
8981
|
-
|
8836
|
+
relative_symlink( # noqa
|
8837
|
+
comp.link_src,
|
8838
|
+
comp.link_dst,
|
8839
|
+
target_is_directory=comp.is_dir,
|
8840
|
+
make_dirs=True,
|
8841
|
+
)
|
8982
8842
|
|
8983
|
-
|
8843
|
+
#
|
8844
|
+
|
8845
|
+
async def write_app_conf(
|
8984
8846
|
self,
|
8985
|
-
spec:
|
8986
|
-
|
8847
|
+
spec: DeployAppConfSpec,
|
8848
|
+
tags: DeployTagMap,
|
8849
|
+
app_conf_dir: str,
|
8850
|
+
conf_link_dir: str,
|
8987
8851
|
) -> None:
|
8988
|
-
|
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
|
+
)
|
8989
8867
|
|
8990
8868
|
|
8991
8869
|
########################################
|
8992
|
-
# ../deploy/paths/
|
8870
|
+
# ../deploy/paths/owners.py
|
8993
8871
|
|
8994
8872
|
|
8995
|
-
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):
|
8996
8883
|
def __init__(
|
8997
8884
|
self,
|
8998
|
-
|
8885
|
+
*args: ta.Any,
|
8886
|
+
owned_dir: str,
|
8999
8887
|
deploy_home: ta.Optional[DeployHome],
|
9000
|
-
|
8888
|
+
**kwargs: ta.Any,
|
9001
8889
|
) -> None:
|
9002
|
-
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)
|
9003
8894
|
|
9004
8895
|
self._deploy_home = deploy_home
|
9005
|
-
|
8896
|
+
|
8897
|
+
self._owned_deploy_paths = frozenset([DeployPath.parse(self._owned_dir + '/')])
|
9006
8898
|
|
9007
8899
|
@cached_nullary
|
9008
|
-
def
|
9009
|
-
|
9010
|
-
for o in self._deploy_path_owners:
|
9011
|
-
for p in o.get_owned_deploy_paths():
|
9012
|
-
if p in dct:
|
9013
|
-
raise DeployPathError(f'Duplicate deploy path owner: {p}')
|
9014
|
-
dct[p] = o
|
9015
|
-
return dct
|
8900
|
+
def _dir(self) -> str:
|
8901
|
+
return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
|
9016
8902
|
|
9017
|
-
|
9018
|
-
|
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
|
9019
8911
|
|
9020
8912
|
|
9021
8913
|
########################################
|
9022
|
-
# ../
|
8914
|
+
# ../remote/_main.py
|
9023
8915
|
|
9024
8916
|
|
9025
|
-
|
9026
|
-
SingleDirDeployPathOwner,
|
9027
|
-
AtomicPathSwapping,
|
9028
|
-
):
|
9029
|
-
def __init__(
|
9030
|
-
self,
|
9031
|
-
*,
|
9032
|
-
deploy_home: ta.Optional[DeployHome] = None,
|
9033
|
-
) -> None:
|
9034
|
-
super().__init__(
|
9035
|
-
owned_dir='tmp',
|
9036
|
-
deploy_home=deploy_home,
|
9037
|
-
)
|
8917
|
+
##
|
9038
8918
|
|
9039
|
-
@cached_nullary
|
9040
|
-
def _swapping(self) -> AtomicPathSwapping:
|
9041
|
-
return TempDirAtomicPathSwapping(
|
9042
|
-
temp_dir=self._make_dir(),
|
9043
|
-
root_dir=check.non_empty_str(self._deploy_home),
|
9044
|
-
)
|
9045
8919
|
|
9046
|
-
|
9047
|
-
|
9048
|
-
|
9049
|
-
|
9050
|
-
**kwargs: ta.Any,
|
9051
|
-
) -> AtomicPathSwap:
|
9052
|
-
return self._swapping().begin_atomic_path_swap(
|
9053
|
-
kind,
|
9054
|
-
dst_path,
|
9055
|
-
**kwargs,
|
9056
|
-
)
|
8920
|
+
class _RemoteExecutionLogHandler(logging.Handler):
|
8921
|
+
def __init__(self, fn: ta.Callable[[str], None]) -> None:
|
8922
|
+
super().__init__()
|
8923
|
+
self._fn = fn
|
9057
8924
|
|
8925
|
+
def emit(self, record):
|
8926
|
+
msg = self.format(record)
|
8927
|
+
self._fn(msg)
|
9058
8928
|
|
9059
|
-
########################################
|
9060
|
-
# ../deploy/venvs.py
|
9061
|
-
"""
|
9062
|
-
TODO:
|
9063
|
-
- interp
|
9064
|
-
- share more code with pyproject?
|
9065
|
-
"""
|
9066
8929
|
|
8930
|
+
##
|
9067
8931
|
|
9068
|
-
|
8932
|
+
|
8933
|
+
class _RemoteExecutionMain:
|
9069
8934
|
def __init__(
|
9070
8935
|
self,
|
9071
|
-
|
9072
|
-
atomics: AtomicPathSwapping,
|
8936
|
+
chan: RemoteChannel,
|
9073
8937
|
) -> None:
|
9074
8938
|
super().__init__()
|
9075
8939
|
|
9076
|
-
self.
|
9077
|
-
|
9078
|
-
async def setup_venv(
|
9079
|
-
self,
|
9080
|
-
spec: DeployVenvSpec,
|
9081
|
-
git_dir: str,
|
9082
|
-
venv_dir: str,
|
9083
|
-
) -> None:
|
9084
|
-
sys_exe = 'python3'
|
9085
|
-
|
9086
|
-
# !! NOTE: (most) venvs cannot be relocated, so an atomic swap can't be used. it's up to the path manager to
|
9087
|
-
# garbage collect orphaned dirs.
|
9088
|
-
await asyncio_subprocesses.check_call(sys_exe, '-m', 'venv', venv_dir)
|
9089
|
-
|
9090
|
-
#
|
9091
|
-
|
9092
|
-
venv_exe = os.path.join(venv_dir, 'bin', 'python3')
|
9093
|
-
|
9094
|
-
#
|
9095
|
-
|
9096
|
-
reqs_txt = os.path.join(git_dir, 'requirements.txt')
|
9097
|
-
|
9098
|
-
if os.path.isfile(reqs_txt):
|
9099
|
-
if spec.use_uv:
|
9100
|
-
await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
|
9101
|
-
pip_cmd = ['-m', 'uv', 'pip']
|
9102
|
-
else:
|
9103
|
-
pip_cmd = ['-m', 'pip']
|
9104
|
-
|
9105
|
-
await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
|
9106
|
-
|
9107
|
-
|
9108
|
-
########################################
|
9109
|
-
# ../remote/_main.py
|
9110
|
-
|
9111
|
-
|
9112
|
-
##
|
9113
|
-
|
9114
|
-
|
9115
|
-
class _RemoteExecutionLogHandler(logging.Handler):
|
9116
|
-
def __init__(self, fn: ta.Callable[[str], None]) -> None:
|
9117
|
-
super().__init__()
|
9118
|
-
self._fn = fn
|
9119
|
-
|
9120
|
-
def emit(self, record):
|
9121
|
-
msg = self.format(record)
|
9122
|
-
self._fn(msg)
|
9123
|
-
|
9124
|
-
|
9125
|
-
##
|
9126
|
-
|
9127
|
-
|
9128
|
-
class _RemoteExecutionMain:
|
9129
|
-
def __init__(
|
9130
|
-
self,
|
9131
|
-
chan: RemoteChannel,
|
9132
|
-
) -> None:
|
9133
|
-
super().__init__()
|
9134
|
-
|
9135
|
-
self._chan = chan
|
8940
|
+
self._chan = chan
|
9136
8941
|
|
9137
8942
|
self.__bootstrap: ta.Optional[MainBootstrap] = None
|
9138
8943
|
self.__injector: ta.Optional[Injector] = None
|
@@ -9638,182 +9443,187 @@ def bind_commands(
|
|
9638
9443
|
|
9639
9444
|
|
9640
9445
|
########################################
|
9641
|
-
# ../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
|
9642
9453
|
|
9454
|
+
github.com/wrmsr/omlish@rev
|
9455
|
+
"""
|
9643
9456
|
|
9644
|
-
|
9457
|
+
|
9458
|
+
##
|
9459
|
+
|
9460
|
+
|
9461
|
+
class DeployGitManager(SingleDirDeployPathOwner):
|
9645
9462
|
def __init__(
|
9646
9463
|
self,
|
9647
9464
|
*,
|
9648
9465
|
deploy_home: ta.Optional[DeployHome] = None,
|
9649
|
-
|
9650
|
-
conf: DeployConfManager,
|
9651
|
-
git: DeployGitManager,
|
9652
|
-
venvs: DeployVenvManager,
|
9466
|
+
atomics: AtomicPathSwapping,
|
9653
9467
|
) -> None:
|
9654
|
-
super().__init__(
|
9655
|
-
|
9656
|
-
|
9657
|
-
|
9658
|
-
self._conf = conf
|
9659
|
-
self._git = git
|
9660
|
-
self._venvs = venvs
|
9468
|
+
super().__init__(
|
9469
|
+
owned_dir='git',
|
9470
|
+
deploy_home=deploy_home,
|
9471
|
+
)
|
9661
9472
|
|
9662
|
-
|
9473
|
+
self._atomics = atomics
|
9663
9474
|
|
9664
|
-
|
9665
|
-
_APP_DIR = DeployPath.parse(_APP_DIR_STR)
|
9475
|
+
self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
|
9666
9476
|
|
9667
|
-
|
9668
|
-
|
9477
|
+
class RepoDir:
|
9478
|
+
def __init__(
|
9479
|
+
self,
|
9480
|
+
git: 'DeployGitManager',
|
9481
|
+
repo: DeployGitRepo,
|
9482
|
+
) -> None:
|
9483
|
+
super().__init__()
|
9669
9484
|
|
9670
|
-
|
9671
|
-
|
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
|
+
)
|
9672
9492
|
|
9673
|
-
|
9674
|
-
|
9675
|
-
|
9676
|
-
self._APP_DIR,
|
9493
|
+
@property
|
9494
|
+
def repo(self) -> DeployGitRepo:
|
9495
|
+
return self._repo
|
9677
9496
|
|
9678
|
-
|
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}'
|
9679
9503
|
|
9680
|
-
|
9681
|
-
self._CONF_DEPLOY_DIR,
|
9504
|
+
#
|
9682
9505
|
|
9683
|
-
|
9684
|
-
|
9685
|
-
|
9686
|
-
|
9687
|
-
|
9688
|
-
'venv',
|
9689
|
-
]
|
9690
|
-
],
|
9691
|
-
}
|
9506
|
+
async def _call(self, *cmd: str) -> None:
|
9507
|
+
await asyncio_subprocesses.check_call(
|
9508
|
+
*cmd,
|
9509
|
+
cwd=self._dir,
|
9510
|
+
)
|
9692
9511
|
|
9693
|
-
|
9512
|
+
#
|
9694
9513
|
|
9695
|
-
|
9696
|
-
|
9697
|
-
|
9698
|
-
|
9699
|
-
|
9700
|
-
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
|
9701
9519
|
|
9702
|
-
|
9703
|
-
|
9520
|
+
await self._call('git', 'init')
|
9521
|
+
await self._call('git', 'remote', 'add', 'origin', self.url)
|
9704
9522
|
|
9705
|
-
|
9706
|
-
|
9707
|
-
|
9523
|
+
async def fetch(self, rev: DeployRev) -> None:
|
9524
|
+
await self.init()
|
9525
|
+
await self._call('git', 'fetch', '--depth=1', 'origin', rev)
|
9708
9526
|
|
9709
9527
|
#
|
9710
9528
|
|
9711
|
-
|
9712
|
-
|
9713
|
-
|
9714
|
-
|
9715
|
-
|
9716
|
-
|
9717
|
-
|
9718
|
-
|
9719
|
-
|
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)
|
9720
9538
|
|
9721
|
-
|
9539
|
+
dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
|
9540
|
+
await dst_call('git', 'init')
|
9722
9541
|
|
9723
|
-
|
9724
|
-
|
9725
|
-
|
9726
|
-
app_deploy_link,
|
9727
|
-
target_is_directory=True,
|
9728
|
-
make_dirs=True,
|
9729
|
-
)
|
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 []))
|
9730
9545
|
|
9731
|
-
|
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
|
9732
9552
|
|
9733
|
-
|
9734
|
-
|
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)
|
9735
9559
|
|
9736
|
-
#
|
9737
9560
|
|
9738
|
-
|
9739
|
-
|
9740
|
-
# check.state(os.path.islink(lp))
|
9741
|
-
# shutil.copy2(
|
9742
|
-
# lp,
|
9743
|
-
# os.path.join(dst, os.path.relpath(lp, src)),
|
9744
|
-
# follow_symlinks=False,
|
9745
|
-
# )
|
9746
|
-
#
|
9747
|
-
# for dp, dns, fns in os.walk(src, followlinks=False):
|
9748
|
-
# for fn in fns:
|
9749
|
-
# mirror_link(os.path.join(dp, fn))
|
9750
|
-
#
|
9751
|
-
# for dn in dns:
|
9752
|
-
# dp2 = os.path.join(dp, dn)
|
9753
|
-
# if os.path.islink(dp2):
|
9754
|
-
# mirror_link(dp2)
|
9755
|
-
# else:
|
9756
|
-
# os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
|
9561
|
+
########################################
|
9562
|
+
# ../deploy/paths/manager.py
|
9757
9563
|
|
9758
|
-
current_link = os.path.join(deploy_home, 'deploys/current')
|
9759
9564
|
|
9760
|
-
|
9761
|
-
|
9762
|
-
|
9763
|
-
|
9764
|
-
|
9765
|
-
|
9766
|
-
|
9767
|
-
|
9768
|
-
# )
|
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__()
|
9769
9573
|
|
9770
|
-
|
9574
|
+
self._deploy_home = deploy_home
|
9575
|
+
self._deploy_path_owners = deploy_path_owners
|
9771
9576
|
|
9772
|
-
|
9773
|
-
|
9774
|
-
|
9775
|
-
|
9776
|
-
|
9777
|
-
|
9778
|
-
|
9779
|
-
|
9780
|
-
|
9781
|
-
app_venv_dir = os.path.join(app_dir, 'venv')
|
9782
|
-
await self._venvs.setup_venv(
|
9783
|
-
spec.venv,
|
9784
|
-
app_git_dir,
|
9785
|
-
app_venv_dir,
|
9786
|
-
)
|
9787
|
-
|
9788
|
-
#
|
9789
|
-
|
9790
|
-
if spec.conf is not None:
|
9791
|
-
app_conf_dir = os.path.join(app_dir, 'conf')
|
9792
|
-
await self._conf.write_app_conf(
|
9793
|
-
spec.conf,
|
9794
|
-
tags,
|
9795
|
-
app_conf_dir,
|
9796
|
-
deploy_conf_dir,
|
9797
|
-
)
|
9798
|
-
|
9799
|
-
#
|
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
|
9800
9586
|
|
9801
|
-
|
9587
|
+
def validate_deploy_paths(self) -> None:
|
9588
|
+
self.owners_by_path()
|
9802
9589
|
|
9803
9590
|
|
9804
9591
|
########################################
|
9805
|
-
# ../deploy/
|
9592
|
+
# ../deploy/tmp.py
|
9806
9593
|
|
9807
9594
|
|
9808
|
-
|
9809
|
-
|
9810
|
-
|
9811
|
-
|
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
|
+
)
|
9812
9608
|
|
9813
|
-
|
9814
|
-
|
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
|
+
)
|
9815
9615
|
|
9816
|
-
|
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
|
+
)
|
9817
9627
|
|
9818
9628
|
|
9819
9629
|
########################################
|
@@ -10531,90 +10341,358 @@ class SystemInterpProvider(InterpProvider):
|
|
10531
10341
|
|
10532
10342
|
|
10533
10343
|
########################################
|
10534
|
-
# ../deploy/
|
10344
|
+
# ../deploy/paths/inject.py
|
10535
10345
|
|
10536
10346
|
|
10537
|
-
|
10347
|
+
def bind_deploy_paths() -> InjectorBindings:
|
10348
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
10349
|
+
inj.bind_array(DeployPathOwner),
|
10350
|
+
inj.bind_array_type(DeployPathOwner, DeployPathOwners),
|
10538
10351
|
|
10352
|
+
inj.bind(DeployPathsManager, singleton=True),
|
10353
|
+
]
|
10539
10354
|
|
10540
|
-
|
10355
|
+
return inj.as_bindings(*lst)
|
10541
10356
|
|
10542
10357
|
|
10543
|
-
|
10544
|
-
|
10545
|
-
self,
|
10546
|
-
*,
|
10547
|
-
apps: DeployAppManager,
|
10548
|
-
paths: DeployPathsManager,
|
10358
|
+
########################################
|
10359
|
+
# ../remote/inject.py
|
10549
10360
|
|
10550
|
-
utc_clock: ta.Optional[DeployManagerUtcClock] = None,
|
10551
|
-
):
|
10552
|
-
super().__init__()
|
10553
10361
|
|
10554
|
-
|
10555
|
-
|
10362
|
+
def bind_remote(
|
10363
|
+
*,
|
10364
|
+
remote_config: RemoteConfig,
|
10365
|
+
) -> InjectorBindings:
|
10366
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
10367
|
+
inj.bind(remote_config),
|
10556
10368
|
|
10557
|
-
|
10369
|
+
inj.bind(SubprocessRemoteSpawning, singleton=True),
|
10370
|
+
inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
|
10558
10371
|
|
10559
|
-
|
10560
|
-
|
10561
|
-
|
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
|
10562
10450
|
else:
|
10563
|
-
return
|
10451
|
+
return ['sh', '-c', get_best_python_sh(), '--']
|
10564
10452
|
|
10565
|
-
def _make_deploy_time(self) -> DeployTime:
|
10566
|
-
return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
|
10567
10453
|
|
10568
|
-
|
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__(
|
10569
10586
|
self,
|
10570
|
-
|
10587
|
+
providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
|
10571
10588
|
) -> None:
|
10572
|
-
|
10589
|
+
super().__init__()
|
10573
10590
|
|
10574
|
-
|
10591
|
+
self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
|
10575
10592
|
|
10576
|
-
|
10577
|
-
|
10578
|
-
|
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,
|
10579
10628
|
)
|
10629
|
+
if not sv:
|
10630
|
+
return None
|
10580
10631
|
|
10581
|
-
|
10632
|
+
bv = sv[-1]
|
10633
|
+
return await tp.install_version(bv)
|
10582
10634
|
|
10583
|
-
|
10584
|
-
|
10585
|
-
|
10586
|
-
|
10587
|
-
|
10588
|
-
|
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}')
|
10589
10647
|
|
10590
|
-
|
10591
|
-
|
10592
|
-
|
10593
|
-
|
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
|
+
]])
|
10594
10671
|
|
10595
10672
|
|
10596
10673
|
########################################
|
10597
|
-
# ../
|
10674
|
+
# ../targets/inject.py
|
10598
10675
|
|
10599
10676
|
|
10600
|
-
def
|
10601
|
-
*,
|
10602
|
-
remote_config: RemoteConfig,
|
10603
|
-
) -> InjectorBindings:
|
10677
|
+
def bind_targets() -> InjectorBindings:
|
10604
10678
|
lst: ta.List[InjectorBindingOrBindings] = [
|
10605
|
-
inj.bind(
|
10606
|
-
|
10607
|
-
inj.bind(
|
10608
|
-
inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
|
10679
|
+
inj.bind(LocalManageTargetConnector, singleton=True),
|
10680
|
+
inj.bind(DockerManageTargetConnector, singleton=True),
|
10681
|
+
inj.bind(SshManageTargetConnector, singleton=True),
|
10609
10682
|
|
10610
|
-
inj.bind(
|
10611
|
-
inj.bind(
|
10683
|
+
inj.bind(TypeSwitchedManageTargetConnector, singleton=True),
|
10684
|
+
inj.bind(ManageTargetConnector, to_key=TypeSwitchedManageTargetConnector),
|
10612
10685
|
]
|
10613
10686
|
|
10614
10687
|
#
|
10615
10688
|
|
10616
|
-
|
10617
|
-
|
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))
|
10618
10696
|
|
10619
10697
|
#
|
10620
10698
|
|
@@ -10622,290 +10700,318 @@ def bind_remote(
|
|
10622
10700
|
|
10623
10701
|
|
10624
10702
|
########################################
|
10625
|
-
# ../
|
10703
|
+
# ../deploy/interp.py
|
10626
10704
|
|
10627
10705
|
|
10628
|
-
|
10629
|
-
*,
|
10630
|
-
system_config: SystemConfig,
|
10631
|
-
) -> InjectorBindings:
|
10632
|
-
lst: ta.List[InjectorBindingOrBindings] = [
|
10633
|
-
inj.bind(system_config),
|
10634
|
-
]
|
10706
|
+
##
|
10635
10707
|
|
10636
|
-
#
|
10637
10708
|
|
10638
|
-
|
10639
|
-
|
10709
|
+
@dc.dataclass(frozen=True)
|
10710
|
+
class InterpCommand(Command['InterpCommand.Output']):
|
10711
|
+
spec: str
|
10712
|
+
install: bool = False
|
10640
10713
|
|
10641
|
-
|
10714
|
+
@dc.dataclass(frozen=True)
|
10715
|
+
class Output(Command.Output):
|
10716
|
+
exe: str
|
10717
|
+
version: str
|
10718
|
+
opts: InterpOpts
|
10642
10719
|
|
10643
|
-
if isinstance(platform, AmazonLinuxPlatform):
|
10644
|
-
lst.extend([
|
10645
|
-
inj.bind(YumSystemPackageManager, singleton=True),
|
10646
|
-
inj.bind(SystemPackageManager, to_key=YumSystemPackageManager),
|
10647
|
-
])
|
10648
10720
|
|
10649
|
-
|
10650
|
-
|
10651
|
-
|
10652
|
-
|
10653
|
-
|
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
|
+
)
|
10654
10730
|
|
10655
|
-
elif isinstance(platform, DarwinPlatform):
|
10656
|
-
lst.extend([
|
10657
|
-
inj.bind(BrewSystemPackageManager, singleton=True),
|
10658
|
-
inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
|
10659
|
-
])
|
10660
10731
|
|
10661
|
-
|
10732
|
+
########################################
|
10733
|
+
# ../deploy/venvs.py
|
10734
|
+
"""
|
10735
|
+
TODO:
|
10736
|
+
- interp
|
10737
|
+
- share more code with pyproject?
|
10738
|
+
"""
|
10662
10739
|
|
10663
|
-
lst.extend([
|
10664
|
-
bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
|
10665
|
-
])
|
10666
10740
|
|
10667
|
-
|
10741
|
+
class DeployVenvManager:
|
10742
|
+
def __init__(
|
10743
|
+
self,
|
10744
|
+
*,
|
10745
|
+
atomics: AtomicPathSwapping,
|
10746
|
+
) -> None:
|
10747
|
+
super().__init__()
|
10668
10748
|
|
10669
|
-
|
10749
|
+
self._atomics = atomics
|
10670
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'
|
10671
10763
|
|
10672
|
-
|
10673
|
-
# ../targets/connection.py
|
10764
|
+
#
|
10674
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)
|
10675
10769
|
|
10676
|
-
|
10770
|
+
#
|
10677
10771
|
|
10772
|
+
venv_exe = os.path.join(venv_dir, 'bin', 'python3')
|
10678
10773
|
|
10679
|
-
|
10680
|
-
@abc.abstractmethod
|
10681
|
-
def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
|
10682
|
-
raise NotImplementedError
|
10774
|
+
#
|
10683
10775
|
|
10684
|
-
|
10685
|
-
check.not_isinstance(python, str)
|
10686
|
-
if python is not None:
|
10687
|
-
return python
|
10688
|
-
else:
|
10689
|
-
return ['sh', '-c', get_best_python_sh(), '--']
|
10776
|
+
reqs_txt = os.path.join(git_dir, 'requirements.txt')
|
10690
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']
|
10691
10784
|
|
10692
|
-
|
10785
|
+
await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
|
10693
10786
|
|
10694
10787
|
|
10695
|
-
|
10788
|
+
########################################
|
10789
|
+
# ../deploy/apps.py
|
10696
10790
|
|
10697
10791
|
|
10698
|
-
|
10699
|
-
|
10700
|
-
|
10792
|
+
class DeployAppManager(DeployPathOwner):
|
10793
|
+
def __init__(
|
10794
|
+
self,
|
10795
|
+
*,
|
10796
|
+
deploy_home: ta.Optional[DeployHome] = None,
|
10701
10797
|
|
10702
|
-
|
10703
|
-
|
10704
|
-
|
10705
|
-
|
10706
|
-
|
10798
|
+
conf: DeployConfManager,
|
10799
|
+
git: DeployGitManager,
|
10800
|
+
venvs: DeployVenvManager,
|
10801
|
+
) -> None:
|
10802
|
+
super().__init__()
|
10707
10803
|
|
10708
|
-
|
10709
|
-
return self.get_connector(type(tgt)).connect(tgt)
|
10804
|
+
self._deploy_home = deploy_home
|
10710
10805
|
|
10806
|
+
self._conf = conf
|
10807
|
+
self._git = git
|
10808
|
+
self._venvs = venvs
|
10711
10809
|
|
10712
|
-
|
10810
|
+
#
|
10713
10811
|
|
10812
|
+
_APP_DIR_STR = 'apps/@app/@time--@app-rev--@app-key/'
|
10813
|
+
_APP_DIR = DeployPath.parse(_APP_DIR_STR)
|
10714
10814
|
|
10715
|
-
|
10716
|
-
|
10717
|
-
_local_executor: LocalCommandExecutor
|
10718
|
-
_in_process_connector: InProcessRemoteExecutionConnector
|
10719
|
-
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10720
|
-
_bootstrap: MainBootstrap
|
10815
|
+
_DEPLOY_DIR_STR = 'deploys/@time--@deploy-key/'
|
10816
|
+
_DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
|
10721
10817
|
|
10722
|
-
|
10723
|
-
|
10724
|
-
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/')
|
10725
10820
|
|
10726
|
-
|
10727
|
-
|
10821
|
+
@cached_nullary
|
10822
|
+
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
10823
|
+
return {
|
10824
|
+
self._APP_DIR,
|
10728
10825
|
|
10729
|
-
|
10730
|
-
yield self._local_executor
|
10826
|
+
self._DEPLOY_DIR,
|
10731
10827
|
|
10732
|
-
|
10733
|
-
|
10734
|
-
yield rce
|
10828
|
+
self._APP_DEPLOY_LINK,
|
10829
|
+
self._CONF_DEPLOY_DIR,
|
10735
10830
|
|
10736
|
-
|
10737
|
-
|
10831
|
+
*[
|
10832
|
+
DeployPath.parse(f'{self._APP_DIR_STR}{sfx}/')
|
10833
|
+
for sfx in [
|
10834
|
+
'conf',
|
10835
|
+
'git',
|
10836
|
+
'venv',
|
10837
|
+
]
|
10838
|
+
],
|
10839
|
+
}
|
10738
10840
|
|
10739
|
-
|
10740
|
-
async with self._pyremote_connector.connect(
|
10741
|
-
RemoteSpawning.Target(
|
10742
|
-
python=self._default_python(lmt.python),
|
10743
|
-
),
|
10744
|
-
self._bootstrap,
|
10745
|
-
) as rce:
|
10746
|
-
yield rce
|
10841
|
+
#
|
10747
10842
|
|
10748
|
-
|
10749
|
-
|
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)
|
10750
10849
|
|
10850
|
+
def build_path(pth: DeployPath) -> str:
|
10851
|
+
return os.path.join(deploy_home, pth.render(tags))
|
10751
10852
|
|
10752
|
-
|
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)
|
10753
10856
|
|
10857
|
+
#
|
10754
10858
|
|
10755
|
-
|
10756
|
-
class DockerManageTargetConnector(ManageTargetConnector):
|
10757
|
-
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10758
|
-
_bootstrap: MainBootstrap
|
10859
|
+
os.makedirs(deploy_dir, exist_ok=True)
|
10759
10860
|
|
10760
|
-
|
10761
|
-
|
10762
|
-
|
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
|
+
)
|
10763
10870
|
|
10764
|
-
|
10765
|
-
if dmt.image is not None:
|
10766
|
-
sh_parts.extend(['run', '-i', dmt.image])
|
10767
|
-
elif dmt.container_id is not None:
|
10768
|
-
sh_parts.extend(['exec', '-i', dmt.container_id])
|
10769
|
-
else:
|
10770
|
-
raise ValueError(dmt)
|
10871
|
+
#
|
10771
10872
|
|
10772
|
-
|
10773
|
-
|
10774
|
-
|
10775
|
-
|
10776
|
-
|
10777
|
-
|
10778
|
-
)
|
10779
|
-
|
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)))
|
10780
10907
|
|
10908
|
+
current_link = os.path.join(deploy_home, 'deploys/current')
|
10781
10909
|
|
10782
|
-
|
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
|
+
# )
|
10783
10919
|
|
10920
|
+
#
|
10784
10921
|
|
10785
|
-
|
10786
|
-
|
10787
|
-
|
10788
|
-
|
10922
|
+
app_git_dir = os.path.join(app_dir, 'git')
|
10923
|
+
await self._git.checkout(
|
10924
|
+
spec.git,
|
10925
|
+
app_git_dir,
|
10926
|
+
)
|
10789
10927
|
|
10790
|
-
|
10791
|
-
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10792
|
-
smt = check.isinstance(tgt, SshManageTarget)
|
10928
|
+
#
|
10793
10929
|
|
10794
|
-
|
10795
|
-
|
10796
|
-
|
10797
|
-
|
10798
|
-
|
10799
|
-
|
10800
|
-
|
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
|
+
)
|
10801
10937
|
|
10802
|
-
|
10803
|
-
RemoteSpawning.Target(
|
10804
|
-
shell=' '.join(sh_parts),
|
10805
|
-
shell_quote=True,
|
10806
|
-
python=self._default_python(smt.python),
|
10807
|
-
),
|
10808
|
-
self._bootstrap,
|
10809
|
-
) as rce:
|
10810
|
-
yield rce
|
10938
|
+
#
|
10811
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
|
+
)
|
10812
10948
|
|
10813
|
-
|
10814
|
-
# ../../../omdev/interp/resolvers.py
|
10949
|
+
#
|
10815
10950
|
|
10951
|
+
os.replace(deploying_link, current_link)
|
10816
10952
|
|
10817
|
-
INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
|
10818
|
-
cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
|
10819
|
-
}
|
10820
10953
|
|
10954
|
+
########################################
|
10955
|
+
# ../deploy/deploy.py
|
10821
10956
|
|
10822
|
-
class InterpResolver:
|
10823
|
-
def __init__(
|
10824
|
-
self,
|
10825
|
-
providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
|
10826
|
-
) -> None:
|
10827
|
-
super().__init__()
|
10828
10957
|
|
10829
|
-
|
10958
|
+
DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
|
10830
10959
|
|
10831
|
-
async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
|
10832
|
-
lst = [
|
10833
|
-
(i, si)
|
10834
|
-
for i, p in enumerate(self._providers.values())
|
10835
|
-
for si in await p.get_installed_versions(spec)
|
10836
|
-
if spec.contains(si)
|
10837
|
-
]
|
10838
10960
|
|
10839
|
-
|
10840
|
-
if not slst:
|
10841
|
-
return None
|
10961
|
+
DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
|
10842
10962
|
|
10843
|
-
bi, bv = slst[-1]
|
10844
|
-
bp = list(self._providers.values())[bi]
|
10845
|
-
return (bp, bv)
|
10846
10963
|
|
10847
|
-
|
10964
|
+
class DeployManager:
|
10965
|
+
def __init__(
|
10848
10966
|
self,
|
10849
|
-
spec: InterpSpecifier,
|
10850
10967
|
*,
|
10851
|
-
|
10852
|
-
|
10853
|
-
tup = await self._resolve_installed(spec)
|
10854
|
-
if tup is not None:
|
10855
|
-
bp, bv = tup
|
10856
|
-
return await bp.get_installed_version(bv)
|
10968
|
+
apps: DeployAppManager,
|
10969
|
+
paths: DeployPathsManager,
|
10857
10970
|
|
10858
|
-
|
10859
|
-
|
10971
|
+
utc_clock: ta.Optional[DeployManagerUtcClock] = None,
|
10972
|
+
):
|
10973
|
+
super().__init__()
|
10860
10974
|
|
10861
|
-
|
10975
|
+
self._apps = apps
|
10976
|
+
self._paths = paths
|
10862
10977
|
|
10863
|
-
|
10864
|
-
[s for s in await tp.get_installable_versions(spec) if s in spec],
|
10865
|
-
key=lambda s: s.version,
|
10866
|
-
)
|
10867
|
-
if not sv:
|
10868
|
-
return None
|
10978
|
+
self._utc_clock = utc_clock
|
10869
10979
|
|
10870
|
-
|
10871
|
-
|
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
|
10872
10985
|
|
10873
|
-
|
10874
|
-
|
10875
|
-
for n, p in self._providers.items():
|
10876
|
-
lst = [
|
10877
|
-
si
|
10878
|
-
for si in await p.get_installed_versions(spec)
|
10879
|
-
if spec.contains(si)
|
10880
|
-
]
|
10881
|
-
if lst:
|
10882
|
-
print(f' {n}')
|
10883
|
-
for si in lst:
|
10884
|
-
print(f' {si}')
|
10986
|
+
def _make_deploy_time(self) -> DeployTime:
|
10987
|
+
return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
|
10885
10988
|
|
10886
|
-
|
10989
|
+
async def run_deploy(
|
10990
|
+
self,
|
10991
|
+
spec: DeploySpec,
|
10992
|
+
) -> None:
|
10993
|
+
self._paths.validate_deploy_paths()
|
10887
10994
|
|
10888
|
-
|
10889
|
-
for n, p in self._providers.items():
|
10890
|
-
lst = [
|
10891
|
-
si
|
10892
|
-
for si in await p.get_installable_versions(spec)
|
10893
|
-
if spec.contains(si)
|
10894
|
-
]
|
10895
|
-
if lst:
|
10896
|
-
print(f' {n}')
|
10897
|
-
for si in lst:
|
10898
|
-
print(f' {si}')
|
10995
|
+
#
|
10899
10996
|
|
10997
|
+
deploy_tags = DeployTagMap(
|
10998
|
+
self._make_deploy_time(),
|
10999
|
+
spec.key(),
|
11000
|
+
)
|
10900
11001
|
|
10901
|
-
|
10902
|
-
# pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
|
10903
|
-
PyenvInterpProvider(try_update=True),
|
11002
|
+
#
|
10904
11003
|
|
10905
|
-
|
11004
|
+
for app in spec.apps:
|
11005
|
+
app_tags = deploy_tags.add(
|
11006
|
+
app.app,
|
11007
|
+
app.key(),
|
11008
|
+
DeployAppRev(app.git.rev),
|
11009
|
+
)
|
10906
11010
|
|
10907
|
-
|
10908
|
-
|
11011
|
+
await self._apps.prepare_app(
|
11012
|
+
app,
|
11013
|
+
app_tags,
|
11014
|
+
)
|
10909
11015
|
|
10910
11016
|
|
10911
11017
|
########################################
|
@@ -10936,65 +11042,6 @@ class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]
|
|
10936
11042
|
return DeployCommand.Output()
|
10937
11043
|
|
10938
11044
|
|
10939
|
-
########################################
|
10940
|
-
# ../targets/inject.py
|
10941
|
-
|
10942
|
-
|
10943
|
-
def bind_targets() -> InjectorBindings:
|
10944
|
-
lst: ta.List[InjectorBindingOrBindings] = [
|
10945
|
-
inj.bind(LocalManageTargetConnector, singleton=True),
|
10946
|
-
inj.bind(DockerManageTargetConnector, singleton=True),
|
10947
|
-
inj.bind(SshManageTargetConnector, singleton=True),
|
10948
|
-
|
10949
|
-
inj.bind(TypeSwitchedManageTargetConnector, singleton=True),
|
10950
|
-
inj.bind(ManageTargetConnector, to_key=TypeSwitchedManageTargetConnector),
|
10951
|
-
]
|
10952
|
-
|
10953
|
-
#
|
10954
|
-
|
10955
|
-
def provide_manage_target_connector_map(injector: Injector) -> ManageTargetConnectorMap:
|
10956
|
-
return ManageTargetConnectorMap({
|
10957
|
-
LocalManageTarget: injector[LocalManageTargetConnector],
|
10958
|
-
DockerManageTarget: injector[DockerManageTargetConnector],
|
10959
|
-
SshManageTarget: injector[SshManageTargetConnector],
|
10960
|
-
})
|
10961
|
-
lst.append(inj.bind(provide_manage_target_connector_map, singleton=True))
|
10962
|
-
|
10963
|
-
#
|
10964
|
-
|
10965
|
-
return inj.as_bindings(*lst)
|
10966
|
-
|
10967
|
-
|
10968
|
-
########################################
|
10969
|
-
# ../deploy/interp.py
|
10970
|
-
|
10971
|
-
|
10972
|
-
##
|
10973
|
-
|
10974
|
-
|
10975
|
-
@dc.dataclass(frozen=True)
|
10976
|
-
class InterpCommand(Command['InterpCommand.Output']):
|
10977
|
-
spec: str
|
10978
|
-
install: bool = False
|
10979
|
-
|
10980
|
-
@dc.dataclass(frozen=True)
|
10981
|
-
class Output(Command.Output):
|
10982
|
-
exe: str
|
10983
|
-
version: str
|
10984
|
-
opts: InterpOpts
|
10985
|
-
|
10986
|
-
|
10987
|
-
class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
|
10988
|
-
async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
|
10989
|
-
i = InterpSpecifier.parse(check.not_none(cmd.spec))
|
10990
|
-
o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
|
10991
|
-
return InterpCommand.Output(
|
10992
|
-
exe=o.exe,
|
10993
|
-
version=str(o.version.version),
|
10994
|
-
opts=o.version.opts,
|
10995
|
-
)
|
10996
|
-
|
10997
|
-
|
10998
11045
|
########################################
|
10999
11046
|
# ../deploy/inject.py
|
11000
11047
|
|
@@ -11143,6 +11190,8 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
|
|
11143
11190
|
|
11144
11191
|
@dc.dataclass(frozen=True)
|
11145
11192
|
class ManageConfig:
|
11193
|
+
deploy_home: ta.Optional[str] = None
|
11194
|
+
|
11146
11195
|
targets: ta.Optional[ta.Mapping[str, ManageTarget]] = None
|
11147
11196
|
|
11148
11197
|
|
@@ -11177,7 +11226,8 @@ class MainCli(ArgparseCli):
|
|
11177
11226
|
argparse_arg('--deploy-home'),
|
11178
11227
|
|
11179
11228
|
argparse_arg('target'),
|
11180
|
-
argparse_arg('command',
|
11229
|
+
argparse_arg('-f', '--command-file', action='append'),
|
11230
|
+
argparse_arg('command', nargs='*'),
|
11181
11231
|
)
|
11182
11232
|
async def run(self) -> None:
|
11183
11233
|
bs = MainBootstrap(
|
@@ -11188,7 +11238,7 @@ class MainCli(ArgparseCli):
|
|
11188
11238
|
),
|
11189
11239
|
|
11190
11240
|
deploy_config=DeployConfig(
|
11191
|
-
deploy_home=self.args.deploy_home,
|
11241
|
+
deploy_home=self.args.deploy_home or self.config().deploy_home,
|
11192
11242
|
),
|
11193
11243
|
|
11194
11244
|
remote_config=RemoteConfig(
|
@@ -11223,13 +11273,19 @@ class MainCli(ArgparseCli):
|
|
11223
11273
|
#
|
11224
11274
|
|
11225
11275
|
cmds: ta.List[Command] = []
|
11276
|
+
|
11226
11277
|
cmd: Command
|
11227
|
-
|
11278
|
+
|
11279
|
+
for c in self.args.command or []:
|
11228
11280
|
if not c.startswith('{'):
|
11229
11281
|
c = json.dumps({c: {}})
|
11230
11282
|
cmd = msh.unmarshal_obj(json.loads(c), Command)
|
11231
11283
|
cmds.append(cmd)
|
11232
11284
|
|
11285
|
+
for cf in self.args.command_file or []:
|
11286
|
+
cmd = read_config_file(cf, Command, msh=msh)
|
11287
|
+
cmds.append(cmd)
|
11288
|
+
|
11233
11289
|
#
|
11234
11290
|
|
11235
11291
|
async with injector[ManageTargetConnector].connect(tgt) as ce:
|