ominfra 0.0.0.dev166__py3-none-any.whl → 0.0.0.dev168__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ominfra/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))
|