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