ominfra 0.0.0.dev166__py3-none-any.whl → 0.0.0.dev168__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- ominfra/manage/deploy/apps.py +112 -11
- ominfra/manage/deploy/commands.py +3 -3
- ominfra/manage/deploy/conf.py +183 -0
- ominfra/manage/deploy/deploy.py +27 -0
- ominfra/manage/deploy/git.py +12 -8
- ominfra/manage/deploy/inject.py +32 -7
- ominfra/manage/deploy/paths/__init__.py +0 -0
- ominfra/manage/deploy/paths/inject.py +21 -0
- ominfra/manage/deploy/paths/manager.py +36 -0
- ominfra/manage/deploy/paths/owners.py +50 -0
- ominfra/manage/deploy/paths/paths.py +216 -0
- ominfra/manage/deploy/specs.py +71 -6
- ominfra/manage/deploy/tmp.py +1 -1
- ominfra/manage/deploy/types.py +26 -1
- ominfra/manage/deploy/venvs.py +4 -33
- ominfra/scripts/journald2aws.py +33 -0
- ominfra/scripts/manage.py +1038 -518
- ominfra/scripts/supervisor.py +34 -0
- ominfra/supervisor/configs.py +1 -0
- {ominfra-0.0.0.dev166.dist-info → ominfra-0.0.0.dev168.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev166.dist-info → ominfra-0.0.0.dev168.dist-info}/RECORD +25 -19
- ominfra/manage/deploy/paths.py +0 -239
- {ominfra-0.0.0.dev166.dist-info → ominfra-0.0.0.dev168.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev166.dist-info → ominfra-0.0.0.dev168.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev166.dist-info → ominfra-0.0.0.dev168.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev166.dist-info → ominfra-0.0.0.dev168.dist-info}/top_level.txt +0 -0
ominfra/scripts/manage.py
CHANGED
@@ -100,9 +100,9 @@ CallableVersionOperator = ta.Callable[['Version', str], bool]
|
|
100
100
|
CommandT = ta.TypeVar('CommandT', bound='Command')
|
101
101
|
CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
|
102
102
|
|
103
|
-
# deploy/
|
103
|
+
# deploy/types.py
|
104
104
|
DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
|
105
|
-
DeployPathPlaceholder = ta.Literal['app', 'tag'] # ta.TypeAlias
|
105
|
+
DeployPathPlaceholder = ta.Literal['app', 'tag', 'conf'] # ta.TypeAlias
|
106
106
|
|
107
107
|
# ../../omlish/argparse/cli.py
|
108
108
|
ArgparseCommandFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
|
@@ -1379,23 +1379,6 @@ class DeployConfig:
|
|
1379
1379
|
deploy_home: ta.Optional[str] = None
|
1380
1380
|
|
1381
1381
|
|
1382
|
-
########################################
|
1383
|
-
# ../deploy/types.py
|
1384
|
-
|
1385
|
-
|
1386
|
-
DeployHome = ta.NewType('DeployHome', str)
|
1387
|
-
|
1388
|
-
DeployApp = ta.NewType('DeployApp', str)
|
1389
|
-
DeployTag = ta.NewType('DeployTag', str)
|
1390
|
-
DeployRev = ta.NewType('DeployRev', str)
|
1391
|
-
DeployKey = ta.NewType('DeployKey', str)
|
1392
|
-
|
1393
|
-
|
1394
|
-
class DeployAppTag(ta.NamedTuple):
|
1395
|
-
app: DeployApp
|
1396
|
-
tag: DeployTag
|
1397
|
-
|
1398
|
-
|
1399
1382
|
########################################
|
1400
1383
|
# ../../pyremote.py
|
1401
1384
|
"""
|
@@ -2074,6 +2057,8 @@ def async_cached_nullary(fn): # ta.Callable[..., T]) -> ta.Callable[..., T]:
|
|
2074
2057
|
"""
|
2075
2058
|
TODO:
|
2076
2059
|
- def maybe(v: lang.Maybe[T])
|
2060
|
+
- def not_ ?
|
2061
|
+
- ** class @dataclass Raise - user message should be able to be an exception type or instance or factory
|
2077
2062
|
"""
|
2078
2063
|
|
2079
2064
|
|
@@ -2732,6 +2717,37 @@ def snake_case(name: str) -> str:
|
|
2732
2717
|
##
|
2733
2718
|
|
2734
2719
|
|
2720
|
+
def strip_with_newline(s: str) -> str:
|
2721
|
+
if not s:
|
2722
|
+
return ''
|
2723
|
+
return s.strip() + '\n'
|
2724
|
+
|
2725
|
+
|
2726
|
+
@ta.overload
|
2727
|
+
def split_keep_delimiter(s: str, d: str) -> str:
|
2728
|
+
...
|
2729
|
+
|
2730
|
+
|
2731
|
+
@ta.overload
|
2732
|
+
def split_keep_delimiter(s: bytes, d: bytes) -> bytes:
|
2733
|
+
...
|
2734
|
+
|
2735
|
+
|
2736
|
+
def split_keep_delimiter(s, d):
|
2737
|
+
ps = []
|
2738
|
+
i = 0
|
2739
|
+
while i < len(s):
|
2740
|
+
if (n := s.find(d, i)) < i:
|
2741
|
+
ps.append(s[i:])
|
2742
|
+
break
|
2743
|
+
ps.append(s[i:n + 1])
|
2744
|
+
i = n + 1
|
2745
|
+
return ps
|
2746
|
+
|
2747
|
+
|
2748
|
+
##
|
2749
|
+
|
2750
|
+
|
2735
2751
|
def is_dunder(name: str) -> bool:
|
2736
2752
|
return (
|
2737
2753
|
name[:2] == name[-2:] == '__' and
|
@@ -3392,6 +3408,42 @@ class LinuxOsRelease:
|
|
3392
3408
|
return dct
|
3393
3409
|
|
3394
3410
|
|
3411
|
+
########################################
|
3412
|
+
# ../../../omlish/os/paths.py
|
3413
|
+
|
3414
|
+
|
3415
|
+
def abs_real_path(p: str) -> str:
|
3416
|
+
return os.path.abspath(os.path.realpath(p))
|
3417
|
+
|
3418
|
+
|
3419
|
+
def is_path_in_dir(base_dir: str, target_path: str) -> bool:
|
3420
|
+
base_dir = abs_real_path(base_dir)
|
3421
|
+
target_path = abs_real_path(target_path)
|
3422
|
+
|
3423
|
+
return target_path.startswith(base_dir + os.path.sep)
|
3424
|
+
|
3425
|
+
|
3426
|
+
def relative_symlink(
|
3427
|
+
src: str,
|
3428
|
+
dst: str,
|
3429
|
+
*,
|
3430
|
+
target_is_directory: bool = False,
|
3431
|
+
dir_fd: ta.Optional[int] = None,
|
3432
|
+
make_dirs: bool = False,
|
3433
|
+
**kwargs: ta.Any,
|
3434
|
+
) -> None:
|
3435
|
+
if make_dirs:
|
3436
|
+
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
3437
|
+
|
3438
|
+
os.symlink(
|
3439
|
+
os.path.relpath(src, os.path.dirname(dst)),
|
3440
|
+
dst,
|
3441
|
+
target_is_directory=target_is_directory,
|
3442
|
+
dir_fd=dir_fd,
|
3443
|
+
**kwargs,
|
3444
|
+
)
|
3445
|
+
|
3446
|
+
|
3395
3447
|
########################################
|
3396
3448
|
# ../../../omdev/packaging/specifiers.py
|
3397
3449
|
# Copyright (c) Donald Stufft and individual contributors.
|
@@ -4065,455 +4117,194 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
|
|
4065
4117
|
|
4066
4118
|
|
4067
4119
|
########################################
|
4068
|
-
# ../deploy/
|
4069
|
-
"""
|
4070
|
-
TODO:
|
4071
|
-
- run/pidfile
|
4072
|
-
- logs/...
|
4073
|
-
- current symlink
|
4074
|
-
- conf/{nginx,supervisor}
|
4075
|
-
- env/?
|
4076
|
-
"""
|
4120
|
+
# ../deploy/types.py
|
4077
4121
|
|
4078
4122
|
|
4079
4123
|
##
|
4080
4124
|
|
4081
4125
|
|
4082
|
-
|
4083
|
-
DEPLOY_PATH_PLACEHOLDER_SEPARATORS = '-.'
|
4126
|
+
DeployHome = ta.NewType('DeployHome', str)
|
4084
4127
|
|
4085
|
-
|
4086
|
-
|
4087
|
-
|
4088
|
-
|
4128
|
+
DeployApp = ta.NewType('DeployApp', str)
|
4129
|
+
DeployTag = ta.NewType('DeployTag', str)
|
4130
|
+
DeployRev = ta.NewType('DeployRev', str)
|
4131
|
+
DeployKey = ta.NewType('DeployKey', str)
|
4089
4132
|
|
4090
4133
|
|
4091
|
-
|
4092
|
-
pass
|
4134
|
+
##
|
4093
4135
|
|
4094
4136
|
|
4095
4137
|
@dc.dataclass(frozen=True)
|
4096
|
-
class
|
4097
|
-
|
4098
|
-
|
4099
|
-
def kind(self) -> DeployPathKind:
|
4100
|
-
raise NotImplementedError
|
4138
|
+
class DeployAppTag:
|
4139
|
+
app: DeployApp
|
4140
|
+
tag: DeployTag
|
4101
4141
|
|
4102
|
-
|
4103
|
-
|
4104
|
-
|
4142
|
+
def __post_init__(self) -> None:
|
4143
|
+
for s in [self.app, self.tag]:
|
4144
|
+
check.non_empty_str(s)
|
4145
|
+
check.equal(s, s.strip())
|
4105
4146
|
|
4147
|
+
def placeholders(self) -> ta.Mapping[DeployPathPlaceholder, str]:
|
4148
|
+
return {
|
4149
|
+
'app': self.app,
|
4150
|
+
'tag': self.tag,
|
4151
|
+
}
|
4106
4152
|
|
4107
|
-
#
|
4108
4153
|
|
4154
|
+
########################################
|
4155
|
+
# ../remote/config.py
|
4109
4156
|
|
4110
|
-
class DirDeployPathPart(DeployPathPart, abc.ABC):
|
4111
|
-
@property
|
4112
|
-
def kind(self) -> DeployPathKind:
|
4113
|
-
return 'dir'
|
4114
4157
|
|
4115
|
-
|
4116
|
-
|
4117
|
-
|
4118
|
-
check.equal(s[0], DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER)
|
4119
|
-
return PlaceholderDirDeployPathPart(s[1:])
|
4120
|
-
else:
|
4121
|
-
return ConstDirDeployPathPart(s)
|
4158
|
+
@dc.dataclass(frozen=True)
|
4159
|
+
class RemoteConfig:
|
4160
|
+
payload_file: ta.Optional[str] = None
|
4122
4161
|
|
4162
|
+
set_pgid: bool = True
|
4123
4163
|
|
4124
|
-
|
4125
|
-
@property
|
4126
|
-
def kind(self) -> DeployPathKind:
|
4127
|
-
return 'file'
|
4164
|
+
deathsig: ta.Optional[str] = 'KILL'
|
4128
4165
|
|
4129
|
-
|
4130
|
-
def parse(cls, s: str) -> 'FileDeployPathPart':
|
4131
|
-
if DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER in s:
|
4132
|
-
check.equal(s[0], DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER)
|
4133
|
-
if not any(c in s for c in DEPLOY_PATH_PLACEHOLDER_SEPARATORS):
|
4134
|
-
return PlaceholderFileDeployPathPart(s[1:], '')
|
4135
|
-
else:
|
4136
|
-
p = min(f for c in DEPLOY_PATH_PLACEHOLDER_SEPARATORS if (f := s.find(c)) > 0)
|
4137
|
-
return PlaceholderFileDeployPathPart(s[1:p], s[p:])
|
4138
|
-
else:
|
4139
|
-
return ConstFileDeployPathPart(s)
|
4166
|
+
pycharm_remote_debug: ta.Optional[PycharmRemoteDebug] = None
|
4140
4167
|
|
4168
|
+
forward_logging: bool = True
|
4141
4169
|
|
4142
|
-
|
4170
|
+
timebomb_delay_s: ta.Optional[float] = 60 * 60.
|
4143
4171
|
|
4172
|
+
heartbeat_interval_s: float = 3.
|
4144
4173
|
|
4145
|
-
@dc.dataclass(frozen=True)
|
4146
|
-
class ConstDeployPathPart(DeployPathPart, abc.ABC):
|
4147
|
-
name: str
|
4148
4174
|
|
4149
|
-
|
4150
|
-
|
4151
|
-
check.not_in('/', self.name)
|
4152
|
-
check.not_in(DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER, self.name)
|
4175
|
+
########################################
|
4176
|
+
# ../remote/payload.py
|
4153
4177
|
|
4154
|
-
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
4155
|
-
return self.name
|
4156
4178
|
|
4179
|
+
RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
|
4157
4180
|
|
4158
|
-
class ConstDirDeployPathPart(ConstDeployPathPart, DirDeployPathPart):
|
4159
|
-
pass
|
4160
4181
|
|
4182
|
+
@cached_nullary
|
4183
|
+
def _get_self_src() -> str:
|
4184
|
+
return inspect.getsource(sys.modules[__name__])
|
4161
4185
|
|
4162
|
-
class ConstFileDeployPathPart(ConstDeployPathPart, FileDeployPathPart):
|
4163
|
-
pass
|
4164
4186
|
|
4187
|
+
def _is_src_amalg(src: str) -> bool:
|
4188
|
+
for l in src.splitlines(): # noqa
|
4189
|
+
if l.startswith('# @omlish-amalg-output '):
|
4190
|
+
return True
|
4191
|
+
return False
|
4165
4192
|
|
4166
|
-
#
|
4167
4193
|
|
4194
|
+
@cached_nullary
|
4195
|
+
def _is_self_amalg() -> bool:
|
4196
|
+
return _is_src_amalg(_get_self_src())
|
4168
4197
|
|
4169
|
-
@dc.dataclass(frozen=True)
|
4170
|
-
class PlaceholderDeployPathPart(DeployPathPart, abc.ABC):
|
4171
|
-
placeholder: str # DeployPathPlaceholder
|
4172
4198
|
|
4173
|
-
|
4174
|
-
|
4175
|
-
|
4176
|
-
|
4177
|
-
|
4199
|
+
def get_remote_payload_src(
|
4200
|
+
*,
|
4201
|
+
file: ta.Optional[RemoteExecutionPayloadFile],
|
4202
|
+
) -> str:
|
4203
|
+
if file is not None:
|
4204
|
+
with open(file) as f:
|
4205
|
+
return f.read()
|
4178
4206
|
|
4179
|
-
|
4180
|
-
|
4181
|
-
return placeholders[self.placeholder] # type: ignore
|
4182
|
-
else:
|
4183
|
-
return DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER + self.placeholder
|
4207
|
+
if _is_self_amalg():
|
4208
|
+
return _get_self_src()
|
4184
4209
|
|
4210
|
+
import importlib.resources
|
4211
|
+
return importlib.resources.files(__package__.split('.')[0] + '.scripts').joinpath('manage.py').read_text()
|
4185
4212
|
|
4186
|
-
@dc.dataclass(frozen=True)
|
4187
|
-
class PlaceholderDirDeployPathPart(PlaceholderDeployPathPart, DirDeployPathPart):
|
4188
|
-
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
4189
|
-
return self._render_placeholder(placeholders)
|
4190
4213
|
|
4214
|
+
########################################
|
4215
|
+
# ../system/platforms.py
|
4191
4216
|
|
4192
|
-
@dc.dataclass(frozen=True)
|
4193
|
-
class PlaceholderFileDeployPathPart(PlaceholderDeployPathPart, FileDeployPathPart):
|
4194
|
-
suffix: str
|
4195
4217
|
|
4196
|
-
|
4197
|
-
super().__post_init__()
|
4198
|
-
if self.suffix:
|
4199
|
-
for c in [DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER, '/']:
|
4200
|
-
check.not_in(c, self.suffix)
|
4218
|
+
##
|
4201
4219
|
|
4202
|
-
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
4203
|
-
return self._render_placeholder(placeholders) + self.suffix
|
4204
4220
|
|
4221
|
+
@dc.dataclass(frozen=True)
|
4222
|
+
class Platform(abc.ABC): # noqa
|
4223
|
+
pass
|
4205
4224
|
|
4206
|
-
##
|
4207
4225
|
|
4226
|
+
class LinuxPlatform(Platform, abc.ABC):
|
4227
|
+
pass
|
4208
4228
|
|
4209
|
-
@dc.dataclass(frozen=True)
|
4210
|
-
class DeployPath:
|
4211
|
-
parts: ta.Sequence[DeployPathPart]
|
4212
4229
|
|
4213
|
-
|
4214
|
-
|
4230
|
+
class UbuntuPlatform(LinuxPlatform):
|
4231
|
+
pass
|
4215
4232
|
|
4216
|
-
check.not_empty(self.parts)
|
4217
|
-
for p in self.parts[:-1]:
|
4218
|
-
check.equal(p.kind, 'dir')
|
4219
4233
|
|
4220
|
-
|
4221
|
-
|
4222
|
-
if isinstance(p, PlaceholderDeployPathPart):
|
4223
|
-
if p.placeholder in pd:
|
4224
|
-
raise DeployPathError('Duplicate placeholders in path', self)
|
4225
|
-
pd[p.placeholder] = i
|
4234
|
+
class AmazonLinuxPlatform(LinuxPlatform):
|
4235
|
+
pass
|
4226
4236
|
|
4227
|
-
if 'tag' in pd:
|
4228
|
-
if 'app' not in pd or pd['app'] >= pd['tag']:
|
4229
|
-
raise DeployPathError('Tag placeholder in path without preceding app', self)
|
4230
4237
|
|
4231
|
-
|
4232
|
-
|
4233
|
-
return self.parts[-1].kind
|
4238
|
+
class GenericLinuxPlatform(LinuxPlatform):
|
4239
|
+
pass
|
4234
4240
|
|
4235
|
-
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
4236
|
-
return os.path.join( # noqa
|
4237
|
-
*[p.render(placeholders) for p in self.parts],
|
4238
|
-
*([''] if self.kind == 'dir' else []),
|
4239
|
-
)
|
4240
4241
|
|
4241
|
-
|
4242
|
-
|
4243
|
-
|
4244
|
-
|
4245
|
-
|
4246
|
-
|
4247
|
-
else:
|
4248
|
-
tail_parse = FileDeployPathPart.parse
|
4249
|
-
ps = check.non_empty_str(s).split('/')
|
4250
|
-
return cls((
|
4251
|
-
*([DirDeployPathPart.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
|
4252
|
-
tail_parse(ps[-1]),
|
4253
|
-
))
|
4242
|
+
class DarwinPlatform(Platform):
|
4243
|
+
pass
|
4244
|
+
|
4245
|
+
|
4246
|
+
class UnknownPlatform(Platform):
|
4247
|
+
pass
|
4254
4248
|
|
4255
4249
|
|
4256
4250
|
##
|
4257
4251
|
|
4258
4252
|
|
4259
|
-
|
4260
|
-
|
4261
|
-
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
4262
|
-
raise NotImplementedError
|
4253
|
+
def _detect_system_platform() -> Platform:
|
4254
|
+
plat = sys.platform
|
4263
4255
|
|
4256
|
+
if plat == 'linux':
|
4257
|
+
if (osr := LinuxOsRelease.read()) is None:
|
4258
|
+
return GenericLinuxPlatform()
|
4264
4259
|
|
4265
|
-
|
4266
|
-
|
4267
|
-
self,
|
4268
|
-
*args: ta.Any,
|
4269
|
-
owned_dir: str,
|
4270
|
-
deploy_home: ta.Optional[DeployHome],
|
4271
|
-
**kwargs: ta.Any,
|
4272
|
-
) -> None:
|
4273
|
-
super().__init__(*args, **kwargs)
|
4260
|
+
if osr.id == 'amzn':
|
4261
|
+
return AmazonLinuxPlatform()
|
4274
4262
|
|
4275
|
-
|
4276
|
-
|
4263
|
+
elif osr.id == 'ubuntu':
|
4264
|
+
return UbuntuPlatform()
|
4277
4265
|
|
4278
|
-
|
4266
|
+
else:
|
4267
|
+
return GenericLinuxPlatform()
|
4279
4268
|
|
4280
|
-
|
4269
|
+
elif plat == 'darwin':
|
4270
|
+
return DarwinPlatform()
|
4281
4271
|
|
4282
|
-
|
4283
|
-
|
4284
|
-
return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
|
4272
|
+
else:
|
4273
|
+
return UnknownPlatform()
|
4285
4274
|
|
4286
|
-
@cached_nullary
|
4287
|
-
def _make_dir(self) -> str:
|
4288
|
-
if not os.path.isdir(d := self._dir()):
|
4289
|
-
os.makedirs(d, exist_ok=True)
|
4290
|
-
return d
|
4291
4275
|
|
4292
|
-
|
4293
|
-
|
4276
|
+
@cached_nullary
|
4277
|
+
def detect_system_platform() -> Platform:
|
4278
|
+
platform = _detect_system_platform()
|
4279
|
+
log.info('Detected platform: %r', platform)
|
4280
|
+
return platform
|
4294
4281
|
|
4295
4282
|
|
4296
4283
|
########################################
|
4297
|
-
# ../
|
4284
|
+
# ../targets/targets.py
|
4285
|
+
"""
|
4286
|
+
It's desugaring. Subprocess and locals are only leafs. Retain an origin?
|
4287
|
+
** TWO LAYERS ** - ManageTarget is user-facing, ConnectorTarget is bound, internal
|
4288
|
+
"""
|
4298
4289
|
|
4299
4290
|
|
4300
4291
|
##
|
4301
4292
|
|
4302
4293
|
|
4303
|
-
|
4304
|
-
|
4305
|
-
|
4306
|
-
|
4307
|
-
|
4294
|
+
class ManageTarget(abc.ABC): # noqa
|
4295
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
4296
|
+
super().__init_subclass__(**kwargs)
|
4297
|
+
|
4298
|
+
check.state(cls.__name__.endswith('ManageTarget'))
|
4308
4299
|
|
4309
|
-
|
4310
|
-
|
4311
|
-
check.not_in('.', check.non_empty_str(self.path))
|
4300
|
+
|
4301
|
+
#
|
4312
4302
|
|
4313
4303
|
|
4314
4304
|
@dc.dataclass(frozen=True)
|
4315
|
-
class
|
4316
|
-
|
4317
|
-
|
4318
|
-
|
4319
|
-
subtrees: ta.Optional[ta.Sequence[str]] = None
|
4320
|
-
|
4321
|
-
def __post_init__(self) -> None:
|
4322
|
-
hash(self)
|
4323
|
-
check.non_empty_str(self.rev)
|
4324
|
-
if self.subtrees is not None:
|
4325
|
-
for st in self.subtrees:
|
4326
|
-
check.non_empty_str(st)
|
4327
|
-
|
4328
|
-
|
4329
|
-
##
|
4330
|
-
|
4331
|
-
|
4332
|
-
@dc.dataclass(frozen=True)
|
4333
|
-
class DeployVenvSpec:
|
4334
|
-
interp: ta.Optional[str] = None
|
4335
|
-
|
4336
|
-
requirements_files: ta.Optional[ta.Sequence[str]] = None
|
4337
|
-
extra_dependencies: ta.Optional[ta.Sequence[str]] = None
|
4338
|
-
|
4339
|
-
use_uv: bool = False
|
4340
|
-
|
4341
|
-
def __post_init__(self) -> None:
|
4342
|
-
hash(self)
|
4343
|
-
|
4344
|
-
|
4345
|
-
##
|
4346
|
-
|
4347
|
-
|
4348
|
-
@dc.dataclass(frozen=True)
|
4349
|
-
class DeploySpec:
|
4350
|
-
app: DeployApp
|
4351
|
-
checkout: DeployGitCheckout
|
4352
|
-
|
4353
|
-
venv: ta.Optional[DeployVenvSpec] = None
|
4354
|
-
|
4355
|
-
def __post_init__(self) -> None:
|
4356
|
-
hash(self)
|
4357
|
-
|
4358
|
-
@cached_nullary
|
4359
|
-
def key(self) -> DeployKey:
|
4360
|
-
return DeployKey(hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8])
|
4361
|
-
|
4362
|
-
|
4363
|
-
########################################
|
4364
|
-
# ../remote/config.py
|
4365
|
-
|
4366
|
-
|
4367
|
-
@dc.dataclass(frozen=True)
|
4368
|
-
class RemoteConfig:
|
4369
|
-
payload_file: ta.Optional[str] = None
|
4370
|
-
|
4371
|
-
set_pgid: bool = True
|
4372
|
-
|
4373
|
-
deathsig: ta.Optional[str] = 'KILL'
|
4374
|
-
|
4375
|
-
pycharm_remote_debug: ta.Optional[PycharmRemoteDebug] = None
|
4376
|
-
|
4377
|
-
forward_logging: bool = True
|
4378
|
-
|
4379
|
-
timebomb_delay_s: ta.Optional[float] = 60 * 60.
|
4380
|
-
|
4381
|
-
heartbeat_interval_s: float = 3.
|
4382
|
-
|
4383
|
-
|
4384
|
-
########################################
|
4385
|
-
# ../remote/payload.py
|
4386
|
-
|
4387
|
-
|
4388
|
-
RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
|
4389
|
-
|
4390
|
-
|
4391
|
-
@cached_nullary
|
4392
|
-
def _get_self_src() -> str:
|
4393
|
-
return inspect.getsource(sys.modules[__name__])
|
4394
|
-
|
4395
|
-
|
4396
|
-
def _is_src_amalg(src: str) -> bool:
|
4397
|
-
for l in src.splitlines(): # noqa
|
4398
|
-
if l.startswith('# @omlish-amalg-output '):
|
4399
|
-
return True
|
4400
|
-
return False
|
4401
|
-
|
4402
|
-
|
4403
|
-
@cached_nullary
|
4404
|
-
def _is_self_amalg() -> bool:
|
4405
|
-
return _is_src_amalg(_get_self_src())
|
4406
|
-
|
4407
|
-
|
4408
|
-
def get_remote_payload_src(
|
4409
|
-
*,
|
4410
|
-
file: ta.Optional[RemoteExecutionPayloadFile],
|
4411
|
-
) -> str:
|
4412
|
-
if file is not None:
|
4413
|
-
with open(file) as f:
|
4414
|
-
return f.read()
|
4415
|
-
|
4416
|
-
if _is_self_amalg():
|
4417
|
-
return _get_self_src()
|
4418
|
-
|
4419
|
-
import importlib.resources
|
4420
|
-
return importlib.resources.files(__package__.split('.')[0] + '.scripts').joinpath('manage.py').read_text()
|
4421
|
-
|
4422
|
-
|
4423
|
-
########################################
|
4424
|
-
# ../system/platforms.py
|
4425
|
-
|
4426
|
-
|
4427
|
-
##
|
4428
|
-
|
4429
|
-
|
4430
|
-
@dc.dataclass(frozen=True)
|
4431
|
-
class Platform(abc.ABC): # noqa
|
4432
|
-
pass
|
4433
|
-
|
4434
|
-
|
4435
|
-
class LinuxPlatform(Platform, abc.ABC):
|
4436
|
-
pass
|
4437
|
-
|
4438
|
-
|
4439
|
-
class UbuntuPlatform(LinuxPlatform):
|
4440
|
-
pass
|
4441
|
-
|
4442
|
-
|
4443
|
-
class AmazonLinuxPlatform(LinuxPlatform):
|
4444
|
-
pass
|
4445
|
-
|
4446
|
-
|
4447
|
-
class GenericLinuxPlatform(LinuxPlatform):
|
4448
|
-
pass
|
4449
|
-
|
4450
|
-
|
4451
|
-
class DarwinPlatform(Platform):
|
4452
|
-
pass
|
4453
|
-
|
4454
|
-
|
4455
|
-
class UnknownPlatform(Platform):
|
4456
|
-
pass
|
4457
|
-
|
4458
|
-
|
4459
|
-
##
|
4460
|
-
|
4461
|
-
|
4462
|
-
def _detect_system_platform() -> Platform:
|
4463
|
-
plat = sys.platform
|
4464
|
-
|
4465
|
-
if plat == 'linux':
|
4466
|
-
if (osr := LinuxOsRelease.read()) is None:
|
4467
|
-
return GenericLinuxPlatform()
|
4468
|
-
|
4469
|
-
if osr.id == 'amzn':
|
4470
|
-
return AmazonLinuxPlatform()
|
4471
|
-
|
4472
|
-
elif osr.id == 'ubuntu':
|
4473
|
-
return UbuntuPlatform()
|
4474
|
-
|
4475
|
-
else:
|
4476
|
-
return GenericLinuxPlatform()
|
4477
|
-
|
4478
|
-
elif plat == 'darwin':
|
4479
|
-
return DarwinPlatform()
|
4480
|
-
|
4481
|
-
else:
|
4482
|
-
return UnknownPlatform()
|
4483
|
-
|
4484
|
-
|
4485
|
-
@cached_nullary
|
4486
|
-
def detect_system_platform() -> Platform:
|
4487
|
-
platform = _detect_system_platform()
|
4488
|
-
log.info('Detected platform: %r', platform)
|
4489
|
-
return platform
|
4490
|
-
|
4491
|
-
|
4492
|
-
########################################
|
4493
|
-
# ../targets/targets.py
|
4494
|
-
"""
|
4495
|
-
It's desugaring. Subprocess and locals are only leafs. Retain an origin?
|
4496
|
-
** TWO LAYERS ** - ManageTarget is user-facing, ConnectorTarget is bound, internal
|
4497
|
-
"""
|
4498
|
-
|
4499
|
-
|
4500
|
-
##
|
4501
|
-
|
4502
|
-
|
4503
|
-
class ManageTarget(abc.ABC): # noqa
|
4504
|
-
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
4505
|
-
super().__init_subclass__(**kwargs)
|
4506
|
-
|
4507
|
-
check.state(cls.__name__.endswith('ManageTarget'))
|
4508
|
-
|
4509
|
-
|
4510
|
-
#
|
4511
|
-
|
4512
|
-
|
4513
|
-
@dc.dataclass(frozen=True)
|
4514
|
-
class PythonRemoteManageTarget:
|
4515
|
-
DEFAULT_PYTHON: ta.ClassVar[str] = 'python3'
|
4516
|
-
python: str = DEFAULT_PYTHON
|
4305
|
+
class PythonRemoteManageTarget:
|
4306
|
+
DEFAULT_PYTHON: ta.ClassVar[str] = 'python3'
|
4307
|
+
python: str = DEFAULT_PYTHON
|
4517
4308
|
|
4518
4309
|
|
4519
4310
|
#
|
@@ -6435,7 +6226,7 @@ class AtomicPathSwapping(abc.ABC):
|
|
6435
6226
|
##
|
6436
6227
|
|
6437
6228
|
|
6438
|
-
class
|
6229
|
+
class OsReplaceAtomicPathSwap(AtomicPathSwap):
|
6439
6230
|
def __init__(
|
6440
6231
|
self,
|
6441
6232
|
kind: AtomicPathSwapKind,
|
@@ -6463,7 +6254,7 @@ class OsRenameAtomicPathSwap(AtomicPathSwap):
|
|
6463
6254
|
return self._tmp_path
|
6464
6255
|
|
6465
6256
|
def _commit(self) -> None:
|
6466
|
-
os.
|
6257
|
+
os.replace(self._tmp_path, self._dst_path)
|
6467
6258
|
|
6468
6259
|
def _abort(self) -> None:
|
6469
6260
|
shutil.rmtree(self._tmp_path, ignore_errors=True)
|
@@ -6510,7 +6301,7 @@ class TempDirAtomicPathSwapping(AtomicPathSwapping):
|
|
6510
6301
|
else:
|
6511
6302
|
raise TypeError(kind)
|
6512
6303
|
|
6513
|
-
return
|
6304
|
+
return OsReplaceAtomicPathSwap(
|
6514
6305
|
kind,
|
6515
6306
|
dst_path,
|
6516
6307
|
tmp_path,
|
@@ -6749,57 +6540,359 @@ CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command
|
|
6749
6540
|
|
6750
6541
|
|
6751
6542
|
########################################
|
6752
|
-
# ../deploy/
|
6753
|
-
|
6754
|
-
|
6755
|
-
|
6756
|
-
|
6757
|
-
|
6758
|
-
|
6759
|
-
|
6760
|
-
|
6761
|
-
|
6762
|
-
deploy_home: ta.Optional[DeployHome] = None,
|
6763
|
-
) -> None:
|
6764
|
-
super().__init__(
|
6765
|
-
owned_dir='tmp',
|
6766
|
-
deploy_home=deploy_home,
|
6767
|
-
)
|
6543
|
+
# ../deploy/paths/paths.py
|
6544
|
+
"""
|
6545
|
+
TODO:
|
6546
|
+
- run/{.pid,.sock}
|
6547
|
+
- logs/...
|
6548
|
+
- current symlink
|
6549
|
+
- conf/{nginx,supervisor}
|
6550
|
+
- env/?
|
6551
|
+
- apps/<app>/shared
|
6552
|
+
"""
|
6768
6553
|
|
6769
|
-
@cached_nullary
|
6770
|
-
def _swapping(self) -> AtomicPathSwapping:
|
6771
|
-
return TempDirAtomicPathSwapping(
|
6772
|
-
temp_dir=self._make_dir(),
|
6773
|
-
root_dir=check.non_empty_str(self._deploy_home),
|
6774
|
-
)
|
6775
6554
|
|
6776
|
-
|
6777
|
-
self,
|
6778
|
-
kind: AtomicPathSwapKind,
|
6779
|
-
dst_path: str,
|
6780
|
-
**kwargs: ta.Any,
|
6781
|
-
) -> AtomicPathSwap:
|
6782
|
-
return self._swapping().begin_atomic_path_swap(
|
6783
|
-
kind,
|
6784
|
-
dst_path,
|
6785
|
-
**kwargs,
|
6786
|
-
)
|
6555
|
+
##
|
6787
6556
|
|
6788
6557
|
|
6789
|
-
|
6790
|
-
|
6558
|
+
DEPLOY_PATH_PLACEHOLDER_SIGIL = '@'
|
6559
|
+
DEPLOY_PATH_PLACEHOLDER_SEPARATOR = '--'
|
6791
6560
|
|
6561
|
+
DEPLOY_PATH_PLACEHOLDER_DELIMITERS: ta.AbstractSet[str] = frozenset([
|
6562
|
+
DEPLOY_PATH_PLACEHOLDER_SEPARATOR,
|
6563
|
+
'.',
|
6564
|
+
])
|
6792
6565
|
|
6793
|
-
|
6794
|
-
|
6795
|
-
|
6566
|
+
DEPLOY_PATH_PLACEHOLDERS: ta.FrozenSet[str] = frozenset([
|
6567
|
+
'app',
|
6568
|
+
'tag',
|
6569
|
+
'conf',
|
6570
|
+
])
|
6796
6571
|
|
6797
6572
|
|
6798
|
-
|
6573
|
+
class DeployPathError(Exception):
|
6574
|
+
pass
|
6799
6575
|
|
6800
6576
|
|
6801
|
-
|
6802
|
-
|
6577
|
+
class DeployPathRenderable(abc.ABC):
|
6578
|
+
@abc.abstractmethod
|
6579
|
+
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
6580
|
+
raise NotImplementedError
|
6581
|
+
|
6582
|
+
|
6583
|
+
##
|
6584
|
+
|
6585
|
+
|
6586
|
+
class DeployPathNamePart(DeployPathRenderable, abc.ABC):
|
6587
|
+
@classmethod
|
6588
|
+
def parse(cls, s: str) -> 'DeployPathNamePart':
|
6589
|
+
check.non_empty_str(s)
|
6590
|
+
if s.startswith(DEPLOY_PATH_PLACEHOLDER_SIGIL):
|
6591
|
+
return PlaceholderDeployPathNamePart(s[1:])
|
6592
|
+
elif s in DEPLOY_PATH_PLACEHOLDER_DELIMITERS:
|
6593
|
+
return DelimiterDeployPathNamePart(s)
|
6594
|
+
else:
|
6595
|
+
return ConstDeployPathNamePart(s)
|
6596
|
+
|
6597
|
+
|
6598
|
+
@dc.dataclass(frozen=True)
|
6599
|
+
class PlaceholderDeployPathNamePart(DeployPathNamePart):
|
6600
|
+
placeholder: str # DeployPathPlaceholder
|
6601
|
+
|
6602
|
+
def __post_init__(self) -> None:
|
6603
|
+
check.in_(self.placeholder, DEPLOY_PATH_PLACEHOLDERS)
|
6604
|
+
|
6605
|
+
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
6606
|
+
if placeholders is not None:
|
6607
|
+
return placeholders[self.placeholder] # type: ignore
|
6608
|
+
else:
|
6609
|
+
return DEPLOY_PATH_PLACEHOLDER_SIGIL + self.placeholder
|
6610
|
+
|
6611
|
+
|
6612
|
+
@dc.dataclass(frozen=True)
|
6613
|
+
class DelimiterDeployPathNamePart(DeployPathNamePart):
|
6614
|
+
delimiter: str
|
6615
|
+
|
6616
|
+
def __post_init__(self) -> None:
|
6617
|
+
check.in_(self.delimiter, DEPLOY_PATH_PLACEHOLDER_DELIMITERS)
|
6618
|
+
|
6619
|
+
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
6620
|
+
return self.delimiter
|
6621
|
+
|
6622
|
+
|
6623
|
+
@dc.dataclass(frozen=True)
|
6624
|
+
class ConstDeployPathNamePart(DeployPathNamePart):
|
6625
|
+
const: str
|
6626
|
+
|
6627
|
+
def __post_init__(self) -> None:
|
6628
|
+
check.non_empty_str(self.const)
|
6629
|
+
for c in [*DEPLOY_PATH_PLACEHOLDER_DELIMITERS, DEPLOY_PATH_PLACEHOLDER_SIGIL, '/']:
|
6630
|
+
check.not_in(c, self.const)
|
6631
|
+
|
6632
|
+
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
6633
|
+
return self.const
|
6634
|
+
|
6635
|
+
|
6636
|
+
@dc.dataclass(frozen=True)
|
6637
|
+
class DeployPathName(DeployPathRenderable):
|
6638
|
+
parts: ta.Sequence[DeployPathNamePart]
|
6639
|
+
|
6640
|
+
def __post_init__(self) -> None:
|
6641
|
+
hash(self)
|
6642
|
+
check.not_empty(self.parts)
|
6643
|
+
for k, g in itertools.groupby(self.parts, type):
|
6644
|
+
if len(gl := list(g)) > 1:
|
6645
|
+
raise DeployPathError(f'May not have consecutive path name part types: {k} {gl}')
|
6646
|
+
|
6647
|
+
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
6648
|
+
return ''.join(p.render(placeholders) for p in self.parts)
|
6649
|
+
|
6650
|
+
@classmethod
|
6651
|
+
def parse(cls, s: str) -> 'DeployPathName':
|
6652
|
+
check.non_empty_str(s)
|
6653
|
+
check.not_in('/', s)
|
6654
|
+
|
6655
|
+
i = 0
|
6656
|
+
ps = []
|
6657
|
+
while i < len(s):
|
6658
|
+
ns = [(n, d) for d in DEPLOY_PATH_PLACEHOLDER_DELIMITERS if (n := s.find(d, i)) >= 0]
|
6659
|
+
if not ns:
|
6660
|
+
ps.append(s[i:])
|
6661
|
+
break
|
6662
|
+
n, d = min(ns)
|
6663
|
+
ps.append(check.non_empty_str(s[i:n]))
|
6664
|
+
ps.append(s[n:n + len(d)])
|
6665
|
+
i = n + len(d)
|
6666
|
+
|
6667
|
+
return cls(tuple(DeployPathNamePart.parse(p) for p in ps))
|
6668
|
+
|
6669
|
+
|
6670
|
+
##
|
6671
|
+
|
6672
|
+
|
6673
|
+
@dc.dataclass(frozen=True)
|
6674
|
+
class DeployPathPart(DeployPathRenderable, abc.ABC): # noqa
|
6675
|
+
name: DeployPathName
|
6676
|
+
|
6677
|
+
@property
|
6678
|
+
@abc.abstractmethod
|
6679
|
+
def kind(self) -> DeployPathKind:
|
6680
|
+
raise NotImplementedError
|
6681
|
+
|
6682
|
+
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
6683
|
+
return self.name.render(placeholders) + ('/' if self.kind == 'dir' else '')
|
6684
|
+
|
6685
|
+
@classmethod
|
6686
|
+
def parse(cls, s: str) -> 'DeployPathPart':
|
6687
|
+
if (is_dir := s.endswith('/')):
|
6688
|
+
s = s[:-1]
|
6689
|
+
check.non_empty_str(s)
|
6690
|
+
check.not_in('/', s)
|
6691
|
+
|
6692
|
+
n = DeployPathName.parse(s)
|
6693
|
+
if is_dir:
|
6694
|
+
return DirDeployPathPart(n)
|
6695
|
+
else:
|
6696
|
+
return FileDeployPathPart(n)
|
6697
|
+
|
6698
|
+
|
6699
|
+
class DirDeployPathPart(DeployPathPart):
|
6700
|
+
@property
|
6701
|
+
def kind(self) -> DeployPathKind:
|
6702
|
+
return 'dir'
|
6703
|
+
|
6704
|
+
|
6705
|
+
class FileDeployPathPart(DeployPathPart):
|
6706
|
+
@property
|
6707
|
+
def kind(self) -> DeployPathKind:
|
6708
|
+
return 'file'
|
6709
|
+
|
6710
|
+
|
6711
|
+
#
|
6712
|
+
|
6713
|
+
|
6714
|
+
@dc.dataclass(frozen=True)
|
6715
|
+
class DeployPath:
|
6716
|
+
parts: ta.Sequence[DeployPathPart]
|
6717
|
+
|
6718
|
+
@property
|
6719
|
+
def name_parts(self) -> ta.Iterator[DeployPathNamePart]:
|
6720
|
+
for p in self.parts:
|
6721
|
+
yield from p.name.parts
|
6722
|
+
|
6723
|
+
def __post_init__(self) -> None:
|
6724
|
+
hash(self)
|
6725
|
+
check.not_empty(self.parts)
|
6726
|
+
for p in self.parts[:-1]:
|
6727
|
+
check.equal(p.kind, 'dir')
|
6728
|
+
|
6729
|
+
pd: ta.Dict[DeployPathPlaceholder, ta.List[int]] = {}
|
6730
|
+
for i, np in enumerate(self.name_parts):
|
6731
|
+
if isinstance(np, PlaceholderDeployPathNamePart):
|
6732
|
+
pd.setdefault(ta.cast(DeployPathPlaceholder, np.placeholder), []).append(i)
|
6733
|
+
|
6734
|
+
# if 'tag' in pd and 'app' not in pd:
|
6735
|
+
# raise DeployPathError('Tag placeholder in path without app', self)
|
6736
|
+
|
6737
|
+
@property
|
6738
|
+
def kind(self) -> ta.Literal['file', 'dir']:
|
6739
|
+
return self.parts[-1].kind
|
6740
|
+
|
6741
|
+
def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
|
6742
|
+
return ''.join([p.render(placeholders) for p in self.parts])
|
6743
|
+
|
6744
|
+
@classmethod
|
6745
|
+
def parse(cls, s: str) -> 'DeployPath':
|
6746
|
+
check.non_empty_str(s)
|
6747
|
+
ps = split_keep_delimiter(s, '/')
|
6748
|
+
return cls(tuple(DeployPathPart.parse(p) for p in ps))
|
6749
|
+
|
6750
|
+
|
6751
|
+
########################################
|
6752
|
+
# ../deploy/specs.py
|
6753
|
+
|
6754
|
+
|
6755
|
+
##
|
6756
|
+
|
6757
|
+
|
6758
|
+
def check_valid_deploy_spec_path(s: str) -> str:
|
6759
|
+
check.non_empty_str(s)
|
6760
|
+
for c in ['..', '//']:
|
6761
|
+
check.not_in(c, s)
|
6762
|
+
check.arg(not s.startswith('/'))
|
6763
|
+
return s
|
6764
|
+
|
6765
|
+
|
6766
|
+
##
|
6767
|
+
|
6768
|
+
|
6769
|
+
@dc.dataclass(frozen=True)
|
6770
|
+
class DeployGitRepo:
|
6771
|
+
host: ta.Optional[str] = None
|
6772
|
+
username: ta.Optional[str] = None
|
6773
|
+
path: ta.Optional[str] = None
|
6774
|
+
|
6775
|
+
def __post_init__(self) -> None:
|
6776
|
+
check.not_in('..', check.non_empty_str(self.host))
|
6777
|
+
check.not_in('.', check.non_empty_str(self.path))
|
6778
|
+
|
6779
|
+
|
6780
|
+
@dc.dataclass(frozen=True)
|
6781
|
+
class DeployGitSpec:
|
6782
|
+
repo: DeployGitRepo
|
6783
|
+
rev: DeployRev
|
6784
|
+
|
6785
|
+
subtrees: ta.Optional[ta.Sequence[str]] = None
|
6786
|
+
|
6787
|
+
def __post_init__(self) -> None:
|
6788
|
+
check.non_empty_str(self.rev)
|
6789
|
+
if self.subtrees is not None:
|
6790
|
+
for st in self.subtrees:
|
6791
|
+
check.non_empty_str(st)
|
6792
|
+
|
6793
|
+
|
6794
|
+
##
|
6795
|
+
|
6796
|
+
|
6797
|
+
@dc.dataclass(frozen=True)
|
6798
|
+
class DeployVenvSpec:
|
6799
|
+
interp: ta.Optional[str] = None
|
6800
|
+
|
6801
|
+
requirements_files: ta.Optional[ta.Sequence[str]] = None
|
6802
|
+
extra_dependencies: ta.Optional[ta.Sequence[str]] = None
|
6803
|
+
|
6804
|
+
use_uv: bool = False
|
6805
|
+
|
6806
|
+
|
6807
|
+
##
|
6808
|
+
|
6809
|
+
|
6810
|
+
@dc.dataclass(frozen=True)
|
6811
|
+
class DeployConfFile:
|
6812
|
+
path: str
|
6813
|
+
body: str
|
6814
|
+
|
6815
|
+
def __post_init__(self) -> None:
|
6816
|
+
check_valid_deploy_spec_path(self.path)
|
6817
|
+
|
6818
|
+
|
6819
|
+
#
|
6820
|
+
|
6821
|
+
|
6822
|
+
@dc.dataclass(frozen=True)
|
6823
|
+
class DeployConfLink(abc.ABC): # noqa
|
6824
|
+
"""
|
6825
|
+
May be either:
|
6826
|
+
- @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
|
6827
|
+
- @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
|
6828
|
+
- @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
|
6829
|
+
"""
|
6830
|
+
|
6831
|
+
src: str
|
6832
|
+
|
6833
|
+
def __post_init__(self) -> None:
|
6834
|
+
check_valid_deploy_spec_path(self.src)
|
6835
|
+
if '/' in self.src:
|
6836
|
+
check.equal(self.src.count('/'), 1)
|
6837
|
+
|
6838
|
+
|
6839
|
+
class AppDeployConfLink(DeployConfLink):
|
6840
|
+
pass
|
6841
|
+
|
6842
|
+
|
6843
|
+
class TagDeployConfLink(DeployConfLink):
|
6844
|
+
pass
|
6845
|
+
|
6846
|
+
|
6847
|
+
#
|
6848
|
+
|
6849
|
+
|
6850
|
+
@dc.dataclass(frozen=True)
|
6851
|
+
class DeployConfSpec:
|
6852
|
+
files: ta.Optional[ta.Sequence[DeployConfFile]] = None
|
6853
|
+
|
6854
|
+
links: ta.Optional[ta.Sequence[DeployConfLink]] = None
|
6855
|
+
|
6856
|
+
def __post_init__(self) -> None:
|
6857
|
+
if self.files:
|
6858
|
+
seen: ta.Set[str] = set()
|
6859
|
+
for f in self.files:
|
6860
|
+
check.not_in(f.path, seen)
|
6861
|
+
seen.add(f.path)
|
6862
|
+
|
6863
|
+
|
6864
|
+
##
|
6865
|
+
|
6866
|
+
|
6867
|
+
@dc.dataclass(frozen=True)
|
6868
|
+
class DeploySpec:
|
6869
|
+
app: DeployApp
|
6870
|
+
|
6871
|
+
git: DeployGitSpec
|
6872
|
+
|
6873
|
+
venv: ta.Optional[DeployVenvSpec] = None
|
6874
|
+
|
6875
|
+
conf: ta.Optional[DeployConfSpec] = None
|
6876
|
+
|
6877
|
+
@cached_nullary
|
6878
|
+
def key(self) -> DeployKey:
|
6879
|
+
return DeployKey(hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8])
|
6880
|
+
|
6881
|
+
|
6882
|
+
########################################
|
6883
|
+
# ../marshal.py
|
6884
|
+
|
6885
|
+
|
6886
|
+
@dc.dataclass(frozen=True)
|
6887
|
+
class ObjMarshalerInstaller:
|
6888
|
+
fn: ta.Callable[[ObjMarshalerManager], None]
|
6889
|
+
|
6890
|
+
|
6891
|
+
ObjMarshalerInstallers = ta.NewType('ObjMarshalerInstallers', ta.Sequence[ObjMarshalerInstaller])
|
6892
|
+
|
6893
|
+
|
6894
|
+
########################################
|
6895
|
+
# ../remote/channel.py
|
6803
6896
|
|
6804
6897
|
|
6805
6898
|
##
|
@@ -7336,38 +7429,253 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
|
|
7336
7429
|
return ret.decode().strip()
|
7337
7430
|
|
7338
7431
|
|
7339
|
-
########################################
|
7340
|
-
# ../bootstrap.py
|
7432
|
+
########################################
|
7433
|
+
# ../bootstrap.py
|
7434
|
+
|
7435
|
+
|
7436
|
+
@dc.dataclass(frozen=True)
|
7437
|
+
class MainBootstrap:
|
7438
|
+
main_config: MainConfig = MainConfig()
|
7439
|
+
|
7440
|
+
deploy_config: DeployConfig = DeployConfig()
|
7441
|
+
|
7442
|
+
remote_config: RemoteConfig = RemoteConfig()
|
7443
|
+
|
7444
|
+
system_config: SystemConfig = SystemConfig()
|
7445
|
+
|
7446
|
+
|
7447
|
+
########################################
|
7448
|
+
# ../commands/local.py
|
7449
|
+
|
7450
|
+
|
7451
|
+
class LocalCommandExecutor(CommandExecutor):
|
7452
|
+
def __init__(
|
7453
|
+
self,
|
7454
|
+
*,
|
7455
|
+
command_executors: CommandExecutorMap,
|
7456
|
+
) -> None:
|
7457
|
+
super().__init__()
|
7458
|
+
|
7459
|
+
self._command_executors = command_executors
|
7460
|
+
|
7461
|
+
async def execute(self, cmd: Command) -> Command.Output:
|
7462
|
+
ce: CommandExecutor = self._command_executors[type(cmd)]
|
7463
|
+
return await ce.execute(cmd)
|
7464
|
+
|
7465
|
+
|
7466
|
+
########################################
|
7467
|
+
# ../deploy/conf.py
|
7468
|
+
"""
|
7469
|
+
TODO:
|
7470
|
+
- @conf DeployPathPlaceholder? :|
|
7471
|
+
- post-deploy: remove any dir_links not present in new spec
|
7472
|
+
- * only if succeeded * - otherwise, remove any dir_links present in new spec but not previously present?
|
7473
|
+
- no such thing as 'previously present'.. build a 'deploy state' and pass it back?
|
7474
|
+
- ** whole thing can be atomic **
|
7475
|
+
- 1) new atomic temp dir
|
7476
|
+
- 2) for each subdir not needing modification, hardlink into temp dir
|
7477
|
+
- 3) for each subdir needing modification, new subdir, hardlink all files not needing modification
|
7478
|
+
- 4) write (or if deleting, omit) new files
|
7479
|
+
- 5) swap top level
|
7480
|
+
- ** whole deploy can be atomic(-ish) - do this for everything **
|
7481
|
+
- just a '/deploy/current' dir
|
7482
|
+
- some things (venvs) cannot be moved, thus the /deploy/venvs dir
|
7483
|
+
- ** ensure (enforce) equivalent relpath nesting
|
7484
|
+
"""
|
7485
|
+
|
7486
|
+
|
7487
|
+
class DeployConfManager:
|
7488
|
+
def __init__(
|
7489
|
+
self,
|
7490
|
+
*,
|
7491
|
+
deploy_home: ta.Optional[DeployHome] = None,
|
7492
|
+
) -> None:
|
7493
|
+
super().__init__()
|
7494
|
+
|
7495
|
+
self._deploy_home = deploy_home
|
7496
|
+
|
7497
|
+
#
|
7498
|
+
|
7499
|
+
async def _write_conf_file(
|
7500
|
+
self,
|
7501
|
+
cf: DeployConfFile,
|
7502
|
+
conf_dir: str,
|
7503
|
+
) -> None:
|
7504
|
+
conf_file = os.path.join(conf_dir, cf.path)
|
7505
|
+
check.arg(is_path_in_dir(conf_dir, conf_file))
|
7506
|
+
|
7507
|
+
os.makedirs(os.path.dirname(conf_file), exist_ok=True)
|
7508
|
+
|
7509
|
+
with open(conf_file, 'w') as f: # noqa
|
7510
|
+
f.write(cf.body)
|
7511
|
+
|
7512
|
+
#
|
7513
|
+
|
7514
|
+
class _ComputedConfLink(ta.NamedTuple):
|
7515
|
+
is_dir: bool
|
7516
|
+
link_src: str
|
7517
|
+
link_dst: str
|
7518
|
+
|
7519
|
+
def _compute_conf_link_dst(
|
7520
|
+
self,
|
7521
|
+
link: DeployConfLink,
|
7522
|
+
app_tag: DeployAppTag,
|
7523
|
+
conf_dir: str,
|
7524
|
+
link_dir: str,
|
7525
|
+
) -> _ComputedConfLink:
|
7526
|
+
link_src = os.path.join(conf_dir, link.src)
|
7527
|
+
check.arg(is_path_in_dir(conf_dir, link_src))
|
7528
|
+
|
7529
|
+
#
|
7530
|
+
|
7531
|
+
if (is_dir := link.src.endswith('/')):
|
7532
|
+
# @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
|
7533
|
+
check.arg(link.src.count('/') == 1)
|
7534
|
+
link_dst_pfx = link.src
|
7535
|
+
link_dst_sfx = ''
|
7536
|
+
|
7537
|
+
elif '/' in link.src:
|
7538
|
+
# @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
|
7539
|
+
d, f = os.path.split(link.src)
|
7540
|
+
# TODO: check filename :|
|
7541
|
+
link_dst_pfx = d + '/'
|
7542
|
+
link_dst_sfx = DEPLOY_PATH_PLACEHOLDER_SEPARATOR + f
|
7543
|
+
|
7544
|
+
else: # noqa
|
7545
|
+
# @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
|
7546
|
+
if '.' in link.src:
|
7547
|
+
l, _, r = link.src.partition('.')
|
7548
|
+
link_dst_pfx = l + '/'
|
7549
|
+
link_dst_sfx = '.' + r
|
7550
|
+
else:
|
7551
|
+
link_dst_pfx = link.src + '/'
|
7552
|
+
link_dst_sfx = ''
|
7553
|
+
|
7554
|
+
#
|
7555
|
+
|
7556
|
+
if isinstance(link, AppDeployConfLink):
|
7557
|
+
link_dst_mid = str(app_tag.app)
|
7558
|
+
elif isinstance(link, TagDeployConfLink):
|
7559
|
+
link_dst_mid = DEPLOY_PATH_PLACEHOLDER_SEPARATOR.join([app_tag.app, app_tag.tag])
|
7560
|
+
else:
|
7561
|
+
raise TypeError(link)
|
7562
|
+
|
7563
|
+
#
|
7564
|
+
|
7565
|
+
link_dst_name = ''.join([
|
7566
|
+
link_dst_pfx,
|
7567
|
+
link_dst_mid,
|
7568
|
+
link_dst_sfx,
|
7569
|
+
])
|
7570
|
+
link_dst = os.path.join(link_dir, link_dst_name)
|
7571
|
+
|
7572
|
+
return DeployConfManager._ComputedConfLink(
|
7573
|
+
is_dir=is_dir,
|
7574
|
+
link_src=link_src,
|
7575
|
+
link_dst=link_dst,
|
7576
|
+
)
|
7577
|
+
|
7578
|
+
async def _make_conf_link(
|
7579
|
+
self,
|
7580
|
+
link: DeployConfLink,
|
7581
|
+
app_tag: DeployAppTag,
|
7582
|
+
conf_dir: str,
|
7583
|
+
link_dir: str,
|
7584
|
+
) -> None:
|
7585
|
+
comp = self._compute_conf_link_dst(
|
7586
|
+
link,
|
7587
|
+
app_tag,
|
7588
|
+
conf_dir,
|
7589
|
+
link_dir,
|
7590
|
+
)
|
7591
|
+
|
7592
|
+
#
|
7593
|
+
|
7594
|
+
check.arg(is_path_in_dir(conf_dir, comp.link_src))
|
7595
|
+
check.arg(is_path_in_dir(link_dir, comp.link_dst))
|
7596
|
+
|
7597
|
+
if comp.is_dir:
|
7598
|
+
check.arg(os.path.isdir(comp.link_src))
|
7599
|
+
else:
|
7600
|
+
check.arg(os.path.isfile(comp.link_src))
|
7601
|
+
|
7602
|
+
#
|
7341
7603
|
|
7604
|
+
relative_symlink( # noqa
|
7605
|
+
comp.link_src,
|
7606
|
+
comp.link_dst,
|
7607
|
+
target_is_directory=comp.is_dir,
|
7608
|
+
make_dirs=True,
|
7609
|
+
)
|
7342
7610
|
|
7343
|
-
|
7344
|
-
class MainBootstrap:
|
7345
|
-
main_config: MainConfig = MainConfig()
|
7611
|
+
#
|
7346
7612
|
|
7347
|
-
|
7613
|
+
async def write_conf(
|
7614
|
+
self,
|
7615
|
+
spec: DeployConfSpec,
|
7616
|
+
app_tag: DeployAppTag,
|
7617
|
+
conf_dir: str,
|
7618
|
+
link_dir: str,
|
7619
|
+
) -> None:
|
7620
|
+
for cf in spec.files or []:
|
7621
|
+
await self._write_conf_file(
|
7622
|
+
cf,
|
7623
|
+
conf_dir,
|
7624
|
+
)
|
7348
7625
|
|
7349
|
-
|
7626
|
+
#
|
7350
7627
|
|
7351
|
-
|
7628
|
+
for link in spec.links or []:
|
7629
|
+
await self._make_conf_link(
|
7630
|
+
link,
|
7631
|
+
app_tag,
|
7632
|
+
conf_dir,
|
7633
|
+
link_dir,
|
7634
|
+
)
|
7352
7635
|
|
7353
7636
|
|
7354
7637
|
########################################
|
7355
|
-
# ../
|
7638
|
+
# ../deploy/paths/owners.py
|
7356
7639
|
|
7357
7640
|
|
7358
|
-
class
|
7641
|
+
class DeployPathOwner(abc.ABC):
|
7642
|
+
@abc.abstractmethod
|
7643
|
+
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
7644
|
+
raise NotImplementedError
|
7645
|
+
|
7646
|
+
|
7647
|
+
DeployPathOwners = ta.NewType('DeployPathOwners', ta.Sequence[DeployPathOwner])
|
7648
|
+
|
7649
|
+
|
7650
|
+
class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
|
7359
7651
|
def __init__(
|
7360
7652
|
self,
|
7361
|
-
|
7362
|
-
|
7653
|
+
*args: ta.Any,
|
7654
|
+
owned_dir: str,
|
7655
|
+
deploy_home: ta.Optional[DeployHome],
|
7656
|
+
**kwargs: ta.Any,
|
7363
7657
|
) -> None:
|
7364
|
-
super().__init__()
|
7658
|
+
super().__init__(*args, **kwargs)
|
7365
7659
|
|
7366
|
-
|
7660
|
+
check.not_in('/', owned_dir)
|
7661
|
+
self._owned_dir: str = check.non_empty_str(owned_dir)
|
7367
7662
|
|
7368
|
-
|
7369
|
-
|
7370
|
-
|
7663
|
+
self._deploy_home = deploy_home
|
7664
|
+
|
7665
|
+
self._owned_deploy_paths = frozenset([DeployPath.parse(self._owned_dir + '/')])
|
7666
|
+
|
7667
|
+
@cached_nullary
|
7668
|
+
def _dir(self) -> str:
|
7669
|
+
return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
|
7670
|
+
|
7671
|
+
@cached_nullary
|
7672
|
+
def _make_dir(self) -> str:
|
7673
|
+
if not os.path.isdir(d := self._dir()):
|
7674
|
+
os.makedirs(d, exist_ok=True)
|
7675
|
+
return d
|
7676
|
+
|
7677
|
+
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
7678
|
+
return self._owned_deploy_paths
|
7371
7679
|
|
7372
7680
|
|
7373
7681
|
########################################
|
@@ -8240,7 +8548,7 @@ class DeployGitManager(SingleDirDeployPathOwner):
|
|
8240
8548
|
|
8241
8549
|
#
|
8242
8550
|
|
8243
|
-
async def checkout(self,
|
8551
|
+
async def checkout(self, spec: DeployGitSpec, dst_dir: str) -> None:
|
8244
8552
|
check.state(not os.path.exists(dst_dir))
|
8245
8553
|
with self._git._atomics.begin_atomic_path_swap( # noqa
|
8246
8554
|
'dir',
|
@@ -8248,14 +8556,14 @@ class DeployGitManager(SingleDirDeployPathOwner):
|
|
8248
8556
|
auto_commit=True,
|
8249
8557
|
make_dirs=True,
|
8250
8558
|
) as dst_swap:
|
8251
|
-
await self.fetch(
|
8559
|
+
await self.fetch(spec.rev)
|
8252
8560
|
|
8253
8561
|
dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
|
8254
8562
|
await dst_call('git', 'init')
|
8255
8563
|
|
8256
8564
|
await dst_call('git', 'remote', 'add', 'local', self._dir)
|
8257
|
-
await dst_call('git', 'fetch', '--depth=1', 'local',
|
8258
|
-
await dst_call('git', 'checkout',
|
8565
|
+
await dst_call('git', 'fetch', '--depth=1', 'local', spec.rev)
|
8566
|
+
await dst_call('git', 'checkout', spec.rev, *(spec.subtrees or []))
|
8259
8567
|
|
8260
8568
|
def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
|
8261
8569
|
try:
|
@@ -8264,8 +8572,80 @@ class DeployGitManager(SingleDirDeployPathOwner):
|
|
8264
8572
|
repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
|
8265
8573
|
return repo_dir
|
8266
8574
|
|
8267
|
-
async def checkout(
|
8268
|
-
|
8575
|
+
async def checkout(
|
8576
|
+
self,
|
8577
|
+
spec: DeployGitSpec,
|
8578
|
+
dst_dir: str,
|
8579
|
+
) -> None:
|
8580
|
+
await self.get_repo_dir(spec.repo).checkout(spec, dst_dir)
|
8581
|
+
|
8582
|
+
|
8583
|
+
########################################
|
8584
|
+
# ../deploy/paths/manager.py
|
8585
|
+
|
8586
|
+
|
8587
|
+
class DeployPathsManager:
|
8588
|
+
def __init__(
|
8589
|
+
self,
|
8590
|
+
*,
|
8591
|
+
deploy_home: ta.Optional[DeployHome],
|
8592
|
+
deploy_path_owners: DeployPathOwners,
|
8593
|
+
) -> None:
|
8594
|
+
super().__init__()
|
8595
|
+
|
8596
|
+
self._deploy_home = deploy_home
|
8597
|
+
self._deploy_path_owners = deploy_path_owners
|
8598
|
+
|
8599
|
+
@cached_nullary
|
8600
|
+
def owners_by_path(self) -> ta.Mapping[DeployPath, DeployPathOwner]:
|
8601
|
+
dct: ta.Dict[DeployPath, DeployPathOwner] = {}
|
8602
|
+
for o in self._deploy_path_owners:
|
8603
|
+
for p in o.get_owned_deploy_paths():
|
8604
|
+
if p in dct:
|
8605
|
+
raise DeployPathError(f'Duplicate deploy path owner: {p}')
|
8606
|
+
dct[p] = o
|
8607
|
+
return dct
|
8608
|
+
|
8609
|
+
def validate_deploy_paths(self) -> None:
|
8610
|
+
self.owners_by_path()
|
8611
|
+
|
8612
|
+
|
8613
|
+
########################################
|
8614
|
+
# ../deploy/tmp.py
|
8615
|
+
|
8616
|
+
|
8617
|
+
class DeployTmpManager(
|
8618
|
+
SingleDirDeployPathOwner,
|
8619
|
+
AtomicPathSwapping,
|
8620
|
+
):
|
8621
|
+
def __init__(
|
8622
|
+
self,
|
8623
|
+
*,
|
8624
|
+
deploy_home: ta.Optional[DeployHome] = None,
|
8625
|
+
) -> None:
|
8626
|
+
super().__init__(
|
8627
|
+
owned_dir='tmp',
|
8628
|
+
deploy_home=deploy_home,
|
8629
|
+
)
|
8630
|
+
|
8631
|
+
@cached_nullary
|
8632
|
+
def _swapping(self) -> AtomicPathSwapping:
|
8633
|
+
return TempDirAtomicPathSwapping(
|
8634
|
+
temp_dir=self._make_dir(),
|
8635
|
+
root_dir=check.non_empty_str(self._deploy_home),
|
8636
|
+
)
|
8637
|
+
|
8638
|
+
def begin_atomic_path_swap(
|
8639
|
+
self,
|
8640
|
+
kind: AtomicPathSwapKind,
|
8641
|
+
dst_path: str,
|
8642
|
+
**kwargs: ta.Any,
|
8643
|
+
) -> AtomicPathSwap:
|
8644
|
+
return self._swapping().begin_atomic_path_swap(
|
8645
|
+
kind,
|
8646
|
+
dst_path,
|
8647
|
+
**kwargs,
|
8648
|
+
)
|
8269
8649
|
|
8270
8650
|
|
8271
8651
|
########################################
|
@@ -8277,32 +8657,21 @@ TODO:
|
|
8277
8657
|
"""
|
8278
8658
|
|
8279
8659
|
|
8280
|
-
class DeployVenvManager
|
8660
|
+
class DeployVenvManager:
|
8281
8661
|
def __init__(
|
8282
8662
|
self,
|
8283
8663
|
*,
|
8284
|
-
deploy_home: ta.Optional[DeployHome] = None,
|
8285
8664
|
atomics: AtomicPathSwapping,
|
8286
8665
|
) -> None:
|
8287
8666
|
super().__init__()
|
8288
8667
|
|
8289
|
-
self._deploy_home = deploy_home
|
8290
8668
|
self._atomics = atomics
|
8291
8669
|
|
8292
|
-
@cached_nullary
|
8293
|
-
def _dir(self) -> str:
|
8294
|
-
return os.path.join(check.non_empty_str(self._deploy_home), 'venvs')
|
8295
|
-
|
8296
|
-
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
8297
|
-
return {
|
8298
|
-
DeployPath.parse('venvs/@app/@tag/'),
|
8299
|
-
}
|
8300
|
-
|
8301
8670
|
async def setup_venv(
|
8302
8671
|
self,
|
8303
|
-
app_dir: str,
|
8304
|
-
venv_dir: str,
|
8305
8672
|
spec: DeployVenvSpec,
|
8673
|
+
git_dir: str,
|
8674
|
+
venv_dir: str,
|
8306
8675
|
) -> None:
|
8307
8676
|
sys_exe = 'python3'
|
8308
8677
|
|
@@ -8316,7 +8685,7 @@ class DeployVenvManager(DeployPathOwner):
|
|
8316
8685
|
|
8317
8686
|
#
|
8318
8687
|
|
8319
|
-
reqs_txt = os.path.join(
|
8688
|
+
reqs_txt = os.path.join(git_dir, 'requirements.txt')
|
8320
8689
|
|
8321
8690
|
if os.path.isfile(reqs_txt):
|
8322
8691
|
if spec.use_uv:
|
@@ -8327,17 +8696,6 @@ class DeployVenvManager(DeployPathOwner):
|
|
8327
8696
|
|
8328
8697
|
await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
|
8329
8698
|
|
8330
|
-
async def setup_app_venv(
|
8331
|
-
self,
|
8332
|
-
app_tag: DeployAppTag,
|
8333
|
-
spec: DeployVenvSpec,
|
8334
|
-
) -> None:
|
8335
|
-
await self.setup_venv(
|
8336
|
-
os.path.join(check.non_empty_str(self._deploy_home), 'apps', app_tag.app, app_tag.tag),
|
8337
|
-
os.path.join(self._dir(), app_tag.app, app_tag.tag),
|
8338
|
-
spec,
|
8339
|
-
)
|
8340
|
-
|
8341
8699
|
|
8342
8700
|
########################################
|
8343
8701
|
# ../remote/_main.py
|
@@ -8886,42 +9244,156 @@ class DeployAppManager(DeployPathOwner):
|
|
8886
9244
|
self,
|
8887
9245
|
*,
|
8888
9246
|
deploy_home: ta.Optional[DeployHome] = None,
|
9247
|
+
|
9248
|
+
conf: DeployConfManager,
|
8889
9249
|
git: DeployGitManager,
|
8890
9250
|
venvs: DeployVenvManager,
|
8891
9251
|
) -> None:
|
8892
9252
|
super().__init__()
|
8893
9253
|
|
8894
9254
|
self._deploy_home = deploy_home
|
9255
|
+
|
9256
|
+
self._conf = conf
|
8895
9257
|
self._git = git
|
8896
9258
|
self._venvs = venvs
|
8897
9259
|
|
8898
|
-
|
8899
|
-
|
8900
|
-
|
9260
|
+
#
|
9261
|
+
|
9262
|
+
_APP_TAG_DIR_STR = 'tags/apps/@app/@tag/'
|
9263
|
+
_APP_TAG_DIR = DeployPath.parse(_APP_TAG_DIR_STR)
|
9264
|
+
|
9265
|
+
_CONF_TAG_DIR_STR = 'tags/conf/@tag--@app/'
|
9266
|
+
_CONF_TAG_DIR = DeployPath.parse(_CONF_TAG_DIR_STR)
|
8901
9267
|
|
9268
|
+
_DEPLOY_DIR_STR = 'deploys/@tag--@app/'
|
9269
|
+
_DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
|
9270
|
+
|
9271
|
+
_APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
|
9272
|
+
_CONF_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf')
|
9273
|
+
|
9274
|
+
@cached_nullary
|
8902
9275
|
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
8903
9276
|
return {
|
8904
|
-
|
9277
|
+
self._APP_TAG_DIR,
|
9278
|
+
|
9279
|
+
self._CONF_TAG_DIR,
|
9280
|
+
|
9281
|
+
self._DEPLOY_DIR,
|
9282
|
+
|
9283
|
+
self._APP_DEPLOY_LINK,
|
9284
|
+
self._CONF_DEPLOY_LINK,
|
9285
|
+
|
9286
|
+
*[
|
9287
|
+
DeployPath.parse(f'{self._APP_TAG_DIR_STR}{sfx}/')
|
9288
|
+
for sfx in [
|
9289
|
+
'conf',
|
9290
|
+
'git',
|
9291
|
+
'venv',
|
9292
|
+
]
|
9293
|
+
],
|
8905
9294
|
}
|
8906
9295
|
|
9296
|
+
#
|
9297
|
+
|
8907
9298
|
async def prepare_app(
|
8908
9299
|
self,
|
8909
9300
|
spec: DeploySpec,
|
8910
9301
|
) -> None:
|
8911
|
-
app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.
|
8912
|
-
|
9302
|
+
app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.git.rev, spec.key()))
|
9303
|
+
|
9304
|
+
#
|
9305
|
+
|
9306
|
+
deploy_home = check.non_empty_str(self._deploy_home)
|
9307
|
+
|
9308
|
+
def build_path(pth: DeployPath) -> str:
|
9309
|
+
return os.path.join(deploy_home, pth.render(app_tag.placeholders()))
|
9310
|
+
|
9311
|
+
app_tag_dir = build_path(self._APP_TAG_DIR)
|
9312
|
+
conf_tag_dir = build_path(self._CONF_TAG_DIR)
|
9313
|
+
deploy_dir = build_path(self._DEPLOY_DIR)
|
9314
|
+
app_deploy_link = build_path(self._APP_DEPLOY_LINK)
|
9315
|
+
conf_deploy_link_file = build_path(self._CONF_DEPLOY_LINK)
|
9316
|
+
|
9317
|
+
#
|
9318
|
+
|
9319
|
+
os.makedirs(deploy_dir)
|
9320
|
+
|
9321
|
+
deploying_link = os.path.join(deploy_home, 'deploys/deploying')
|
9322
|
+
relative_symlink(
|
9323
|
+
deploy_dir,
|
9324
|
+
deploying_link,
|
9325
|
+
target_is_directory=True,
|
9326
|
+
make_dirs=True,
|
9327
|
+
)
|
9328
|
+
|
9329
|
+
#
|
9330
|
+
|
9331
|
+
os.makedirs(app_tag_dir)
|
9332
|
+
relative_symlink(
|
9333
|
+
app_tag_dir,
|
9334
|
+
app_deploy_link,
|
9335
|
+
target_is_directory=True,
|
9336
|
+
make_dirs=True,
|
9337
|
+
)
|
9338
|
+
|
9339
|
+
#
|
9340
|
+
|
9341
|
+
os.makedirs(conf_tag_dir)
|
9342
|
+
relative_symlink(
|
9343
|
+
conf_tag_dir,
|
9344
|
+
conf_deploy_link_file,
|
9345
|
+
target_is_directory=True,
|
9346
|
+
make_dirs=True,
|
9347
|
+
)
|
8913
9348
|
|
8914
9349
|
#
|
8915
9350
|
|
9351
|
+
git_dir = os.path.join(app_tag_dir, 'git')
|
8916
9352
|
await self._git.checkout(
|
8917
|
-
spec.
|
8918
|
-
|
9353
|
+
spec.git,
|
9354
|
+
git_dir,
|
8919
9355
|
)
|
8920
9356
|
|
8921
9357
|
#
|
8922
9358
|
|
8923
9359
|
if spec.venv is not None:
|
8924
|
-
|
9360
|
+
venv_dir = os.path.join(app_tag_dir, 'venv')
|
9361
|
+
await self._venvs.setup_venv(
|
9362
|
+
spec.venv,
|
9363
|
+
git_dir,
|
9364
|
+
venv_dir,
|
9365
|
+
)
|
9366
|
+
|
9367
|
+
#
|
9368
|
+
|
9369
|
+
if spec.conf is not None:
|
9370
|
+
conf_dir = os.path.join(app_tag_dir, 'conf')
|
9371
|
+
await self._conf.write_conf(
|
9372
|
+
spec.conf,
|
9373
|
+
app_tag,
|
9374
|
+
conf_dir,
|
9375
|
+
conf_tag_dir,
|
9376
|
+
)
|
9377
|
+
|
9378
|
+
#
|
9379
|
+
|
9380
|
+
current_link = os.path.join(deploy_home, 'deploys/current')
|
9381
|
+
os.replace(deploying_link, current_link)
|
9382
|
+
|
9383
|
+
|
9384
|
+
########################################
|
9385
|
+
# ../deploy/paths/inject.py
|
9386
|
+
|
9387
|
+
|
9388
|
+
def bind_deploy_paths() -> InjectorBindings:
|
9389
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
9390
|
+
inj.bind_array(DeployPathOwner),
|
9391
|
+
inj.bind_array_type(DeployPathOwner, DeployPathOwners),
|
9392
|
+
|
9393
|
+
inj.bind(DeployPathsManager, singleton=True),
|
9394
|
+
]
|
9395
|
+
|
9396
|
+
return inj.as_bindings(*lst)
|
8925
9397
|
|
8926
9398
|
|
8927
9399
|
########################################
|
@@ -9639,31 +10111,30 @@ class SystemInterpProvider(InterpProvider):
|
|
9639
10111
|
|
9640
10112
|
|
9641
10113
|
########################################
|
9642
|
-
# ../deploy/
|
9643
|
-
|
9644
|
-
|
9645
|
-
##
|
9646
|
-
|
9647
|
-
|
9648
|
-
@dc.dataclass(frozen=True)
|
9649
|
-
class DeployCommand(Command['DeployCommand.Output']):
|
9650
|
-
spec: DeploySpec
|
10114
|
+
# ../deploy/deploy.py
|
9651
10115
|
|
9652
|
-
@dc.dataclass(frozen=True)
|
9653
|
-
class Output(Command.Output):
|
9654
|
-
pass
|
9655
10116
|
|
10117
|
+
class DeployManager:
|
10118
|
+
def __init__(
|
10119
|
+
self,
|
10120
|
+
*,
|
10121
|
+
apps: DeployAppManager,
|
10122
|
+
paths: DeployPathsManager,
|
10123
|
+
):
|
10124
|
+
super().__init__()
|
9656
10125
|
|
9657
|
-
|
9658
|
-
|
9659
|
-
_apps: DeployAppManager
|
10126
|
+
self._apps = apps
|
10127
|
+
self._paths = paths
|
9660
10128
|
|
9661
|
-
async def
|
9662
|
-
|
10129
|
+
async def run_deploy(
|
10130
|
+
self,
|
10131
|
+
spec: DeploySpec,
|
10132
|
+
) -> None:
|
10133
|
+
self._paths.validate_deploy_paths()
|
9663
10134
|
|
9664
|
-
|
10135
|
+
#
|
9665
10136
|
|
9666
|
-
|
10137
|
+
await self._apps.prepare_app(spec)
|
9667
10138
|
|
9668
10139
|
|
9669
10140
|
########################################
|
@@ -9974,6 +10445,34 @@ DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
|
|
9974
10445
|
]])
|
9975
10446
|
|
9976
10447
|
|
10448
|
+
########################################
|
10449
|
+
# ../deploy/commands.py
|
10450
|
+
|
10451
|
+
|
10452
|
+
##
|
10453
|
+
|
10454
|
+
|
10455
|
+
@dc.dataclass(frozen=True)
|
10456
|
+
class DeployCommand(Command['DeployCommand.Output']):
|
10457
|
+
spec: DeploySpec
|
10458
|
+
|
10459
|
+
@dc.dataclass(frozen=True)
|
10460
|
+
class Output(Command.Output):
|
10461
|
+
pass
|
10462
|
+
|
10463
|
+
|
10464
|
+
@dc.dataclass(frozen=True)
|
10465
|
+
class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
|
10466
|
+
_deploy: DeployManager
|
10467
|
+
|
10468
|
+
async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
|
10469
|
+
log.info('Deploying! %r', cmd.spec)
|
10470
|
+
|
10471
|
+
await self._deploy.run_deploy(cmd.spec)
|
10472
|
+
|
10473
|
+
return DeployCommand.Output()
|
10474
|
+
|
10475
|
+
|
9977
10476
|
########################################
|
9978
10477
|
# ../targets/inject.py
|
9979
10478
|
|
@@ -10044,22 +10543,43 @@ def bind_deploy(
|
|
10044
10543
|
lst: ta.List[InjectorBindingOrBindings] = [
|
10045
10544
|
inj.bind(deploy_config),
|
10046
10545
|
|
10047
|
-
|
10546
|
+
bind_deploy_paths(),
|
10547
|
+
]
|
10548
|
+
|
10549
|
+
#
|
10048
10550
|
|
10049
|
-
|
10551
|
+
def bind_manager(cls: type) -> InjectorBindings:
|
10552
|
+
return inj.as_bindings(
|
10553
|
+
inj.bind(cls, singleton=True),
|
10050
10554
|
|
10051
|
-
|
10555
|
+
*([inj.bind(DeployPathOwner, to_key=cls, array=True)] if issubclass(cls, DeployPathOwner) else []),
|
10556
|
+
)
|
10557
|
+
|
10558
|
+
#
|
10559
|
+
|
10560
|
+
lst.extend([
|
10561
|
+
bind_manager(DeployAppManager),
|
10562
|
+
|
10563
|
+
bind_manager(DeployConfManager),
|
10564
|
+
|
10565
|
+
bind_manager(DeployGitManager),
|
10566
|
+
|
10567
|
+
bind_manager(DeployManager),
|
10052
10568
|
|
10053
|
-
|
10569
|
+
bind_manager(DeployTmpManager),
|
10054
10570
|
inj.bind(AtomicPathSwapping, to_key=DeployTmpManager),
|
10055
10571
|
|
10056
|
-
|
10572
|
+
bind_manager(DeployVenvManager),
|
10573
|
+
])
|
10057
10574
|
|
10058
|
-
|
10575
|
+
#
|
10059
10576
|
|
10577
|
+
lst.extend([
|
10060
10578
|
bind_command(DeployCommand, DeployCommandExecutor),
|
10061
10579
|
bind_command(InterpCommand, InterpCommandExecutor),
|
10062
|
-
]
|
10580
|
+
])
|
10581
|
+
|
10582
|
+
#
|
10063
10583
|
|
10064
10584
|
if (dh := deploy_config.deploy_home) is not None:
|
10065
10585
|
dh = os.path.abspath(os.path.expanduser(dh))
|