outerbounds 0.3.180rc5__py3-none-any.whl → 0.3.181__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.
- outerbounds/__init__.py +1 -3
- outerbounds/command_groups/apps_cli.py +6 -2
- outerbounds/command_groups/cli.py +2 -0
- outerbounds/command_groups/kubernetes_cli.py +479 -0
- {outerbounds-0.3.180rc5.dist-info → outerbounds-0.3.181.dist-info}/METADATA +3 -3
- {outerbounds-0.3.180rc5.dist-info → outerbounds-0.3.181.dist-info}/RECORD +8 -28
- outerbounds-0.3.181.dist-info/entry_points.txt +3 -0
- outerbounds/_vendor/spinner/__init__.py +0 -4
- outerbounds/_vendor/spinner/spinners.py +0 -478
- outerbounds/_vendor/spinner.LICENSE +0 -21
- outerbounds/apps/__init__.py +0 -0
- outerbounds/apps/_state_machine.py +0 -358
- outerbounds/apps/app_cli.py +0 -1312
- outerbounds/apps/app_config.py +0 -293
- outerbounds/apps/artifacts.py +0 -0
- outerbounds/apps/capsule.py +0 -798
- outerbounds/apps/cli_to_config.py +0 -99
- outerbounds/apps/code_package/__init__.py +0 -3
- outerbounds/apps/code_package/code_packager.py +0 -610
- outerbounds/apps/code_package/examples.py +0 -125
- outerbounds/apps/config_schema.yaml +0 -264
- outerbounds/apps/dependencies.py +0 -115
- outerbounds/apps/deployer.py +0 -0
- outerbounds/apps/experimental/__init__.py +0 -103
- outerbounds/apps/perimeters.py +0 -51
- outerbounds/apps/secrets.py +0 -164
- outerbounds/apps/utils.py +0 -386
- outerbounds/apps/validations.py +0 -22
- outerbounds-0.3.180rc5.dist-info/entry_points.txt +0 -3
- {outerbounds-0.3.180rc5.dist-info → outerbounds-0.3.181.dist-info}/WHEEL +0 -0
@@ -1,358 +0,0 @@
|
|
1
|
-
from typing import List, Tuple, Dict, Union
|
2
|
-
|
3
|
-
|
4
|
-
class _dagNode:
|
5
|
-
def __init__(self, name: str):
|
6
|
-
self.name = name
|
7
|
-
self.incoming_nodes: List["_dagNode"] = []
|
8
|
-
self.outgoing_nodes: List["_dagNode"] = []
|
9
|
-
|
10
|
-
def goto(self, *nodes: "_dagNode"):
|
11
|
-
for node in nodes:
|
12
|
-
self.outgoing_nodes.append(node)
|
13
|
-
node.incoming_nodes.append(self)
|
14
|
-
return self
|
15
|
-
|
16
|
-
def arrives_from(self, *nodes: "_dagNode"):
|
17
|
-
for node in nodes:
|
18
|
-
node.outgoing_nodes.append(self)
|
19
|
-
self.incoming_nodes.append(node)
|
20
|
-
return self
|
21
|
-
|
22
|
-
def __repr__(self):
|
23
|
-
return self.name
|
24
|
-
|
25
|
-
def __str__(self):
|
26
|
-
return self.name
|
27
|
-
|
28
|
-
|
29
|
-
class _capsuleDeployerStateMachine:
|
30
|
-
def __init__(self):
|
31
|
-
# -- (your existing setup) --
|
32
|
-
start_state = _dagNode("start")
|
33
|
-
fail_state = _dagNode("fail")
|
34
|
-
success_state = _dagNode("success")
|
35
|
-
upgrade_state = _dagNode("upgrade")
|
36
|
-
first_time_create_state = _dagNode("first_time_create")
|
37
|
-
end_state = _dagNode("end")
|
38
|
-
|
39
|
-
capsule_deploy_api_call = _dagNode("capsule_deploy_api_call")
|
40
|
-
capsule_deploy_api_call_rejected = _dagNode("capsule_deploy_api_call_rejected")
|
41
|
-
capsule_worker_pending = _dagNode("capsule_worker_pending")
|
42
|
-
|
43
|
-
capsule_single_worker_ready = _dagNode("capsule_single_worker_ready")
|
44
|
-
capsule_multiple_workers_ready = _dagNode("capsule_all_workers_ready")
|
45
|
-
current_deployment_deployed_worker_crashed = _dagNode(
|
46
|
-
"current_deployment_deployed_worker_crashed"
|
47
|
-
)
|
48
|
-
current_deployment_workers_pending_beyond_timeout = _dagNode(
|
49
|
-
"current_deployment_workers_pending_beyond_timeout"
|
50
|
-
)
|
51
|
-
|
52
|
-
start_state.goto(first_time_create_state, upgrade_state)
|
53
|
-
|
54
|
-
capsule_deploy_api_call.arrives_from(
|
55
|
-
first_time_create_state, upgrade_state
|
56
|
-
).goto(capsule_deploy_api_call_rejected, capsule_worker_pending)
|
57
|
-
|
58
|
-
capsule_worker_pending.goto(
|
59
|
-
capsule_single_worker_ready,
|
60
|
-
capsule_multiple_workers_ready,
|
61
|
-
current_deployment_deployed_worker_crashed,
|
62
|
-
current_deployment_workers_pending_beyond_timeout,
|
63
|
-
)
|
64
|
-
success_state.arrives_from(
|
65
|
-
capsule_single_worker_ready, capsule_multiple_workers_ready
|
66
|
-
).goto(end_state)
|
67
|
-
fail_state.arrives_from(
|
68
|
-
capsule_deploy_api_call_rejected,
|
69
|
-
current_deployment_deployed_worker_crashed,
|
70
|
-
current_deployment_workers_pending_beyond_timeout,
|
71
|
-
).goto(end_state)
|
72
|
-
|
73
|
-
self._states = [
|
74
|
-
start_state,
|
75
|
-
fail_state,
|
76
|
-
success_state,
|
77
|
-
upgrade_state,
|
78
|
-
first_time_create_state,
|
79
|
-
end_state,
|
80
|
-
capsule_single_worker_ready,
|
81
|
-
capsule_multiple_workers_ready,
|
82
|
-
current_deployment_deployed_worker_crashed,
|
83
|
-
current_deployment_workers_pending_beyond_timeout,
|
84
|
-
capsule_deploy_api_call,
|
85
|
-
capsule_deploy_api_call_rejected,
|
86
|
-
capsule_worker_pending,
|
87
|
-
]
|
88
|
-
|
89
|
-
def get_edges(self) -> List[Tuple["_dagNode", "_dagNode"]]:
|
90
|
-
"""
|
91
|
-
Returns a list of (src_node, dst_node) tuples for all transitions.
|
92
|
-
"""
|
93
|
-
edges = []
|
94
|
-
for node in self._states:
|
95
|
-
for out in node.outgoing_nodes:
|
96
|
-
edges.append((node, out))
|
97
|
-
return edges
|
98
|
-
|
99
|
-
def to_dot(self, graph_name="StateMachine"):
|
100
|
-
"""
|
101
|
-
Emit a Graphviz DOT description of the state machine.
|
102
|
-
"""
|
103
|
-
lines = [f"digraph {graph_name} {{"]
|
104
|
-
# optional: rankdir=LR for left-to-right layout
|
105
|
-
lines.append(" rankdir=LR;")
|
106
|
-
for src, dst in self.get_edges():
|
107
|
-
lines.append(f' "{src}" -> "{dst}";')
|
108
|
-
lines.append("}")
|
109
|
-
return "\n".join(lines)
|
110
|
-
|
111
|
-
def adjacency_list(self):
|
112
|
-
"""
|
113
|
-
Returns a dict mapping each node to list of its outgoing nodes.
|
114
|
-
"""
|
115
|
-
return {node: list(node.outgoing_nodes) for node in self._states}
|
116
|
-
|
117
|
-
def __str__(self):
|
118
|
-
# Default to DOT format; you could swap this out for something else
|
119
|
-
return self.to_dot()
|
120
|
-
|
121
|
-
def to_diagraph(self):
|
122
|
-
from graphviz import Digraph
|
123
|
-
|
124
|
-
# Create a new Digraph
|
125
|
-
dot = Digraph(name="StateMachine", format="png")
|
126
|
-
dot.attr(rankdir="LR") # left-to-right layout
|
127
|
-
|
128
|
-
# Add one edge per transition in your SM
|
129
|
-
for src, dst in self.get_edges():
|
130
|
-
# src and dst are _dagNode instances; use their .name (or str(src))
|
131
|
-
dot.edge(src.name, dst.name)
|
132
|
-
|
133
|
-
# Render to file (e.g. "state_machine.png") and optionally view it:
|
134
|
-
dot.render("state_machine", view=False)
|
135
|
-
|
136
|
-
|
137
|
-
from typing import TypedDict
|
138
|
-
|
139
|
-
|
140
|
-
class WorkerStatus(TypedDict):
|
141
|
-
workerId: str
|
142
|
-
phase: str
|
143
|
-
activity: int
|
144
|
-
activityDataAvailable: bool
|
145
|
-
version: str
|
146
|
-
|
147
|
-
|
148
|
-
from typing import Dict, List, TypedDict
|
149
|
-
|
150
|
-
|
151
|
-
class WorkerInfoDict(TypedDict):
|
152
|
-
# TODO : Check if we need to account for the `Terminating` state
|
153
|
-
pending: Dict[str, List[WorkerStatus]]
|
154
|
-
running: Dict[str, List[WorkerStatus]]
|
155
|
-
crashlooping: Dict[str, List[WorkerStatus]]
|
156
|
-
|
157
|
-
|
158
|
-
class CurrentWorkerInfo(TypedDict):
|
159
|
-
# TODO : Check if we need to account for the `Terminating` state
|
160
|
-
pending: int
|
161
|
-
running: int
|
162
|
-
crashlooping: int
|
163
|
-
|
164
|
-
|
165
|
-
class DEPLOYMENT_READY_CONDITIONS:
|
166
|
-
"""
|
167
|
-
Deployment ready conditions define what is considered a successful completion of a deployment.
|
168
|
-
This allows users or platform designers to configure the criteria for deployment readiness.
|
169
|
-
|
170
|
-
Reasons for different deployment modes include:
|
171
|
-
1) [at_least_one_running] Some endpoints may be deployed ephemerally and are considered ready when at least one instance is running; additional instances are for load management.
|
172
|
-
2) [all_running] Operators may require that all replicas are available, running, and only the current deployment version is serving traffic.
|
173
|
-
3) [fully_finished] Operators may only care that the minimum number of replicas for the current rollout are present in the cluster.
|
174
|
-
"""
|
175
|
-
|
176
|
-
# `ATLEAST_ONE_RUNNING` implies that atleast one worker of the current deployment instance's version has started running.
|
177
|
-
ATLEAST_ONE_RUNNING = "at_least_one_running"
|
178
|
-
|
179
|
-
# `ALL_RUNNING` implies that all workers of the current deployment instance's version have started running (i.e. all workers aligning to the minimum number of replicas).
|
180
|
-
# It doesn't imply that all the workers relating to other deployments have been torn down.
|
181
|
-
ALL_RUNNING = "all_running"
|
182
|
-
|
183
|
-
# `FULLY_FINISHED` implies that the deployment has the minimum number of replicas and all the workers are related to the current deployment instance's version.
|
184
|
-
FULLY_FINISHED = "fully_finished"
|
185
|
-
|
186
|
-
@classmethod
|
187
|
-
def docstring(cls):
|
188
|
-
return cls.__doc__
|
189
|
-
|
190
|
-
@classmethod
|
191
|
-
def enums(cls):
|
192
|
-
return [
|
193
|
-
cls.ATLEAST_ONE_RUNNING,
|
194
|
-
cls.ALL_RUNNING,
|
195
|
-
cls.FULLY_FINISHED,
|
196
|
-
]
|
197
|
-
|
198
|
-
|
199
|
-
class CapsuleWorkerStatusDict(TypedDict):
|
200
|
-
at_least_one_pending: bool
|
201
|
-
at_least_one_running: bool
|
202
|
-
at_least_one_crashlooping: bool
|
203
|
-
all_running: bool
|
204
|
-
fully_finished: bool
|
205
|
-
none_present: bool
|
206
|
-
current_info: CurrentWorkerInfo
|
207
|
-
|
208
|
-
|
209
|
-
class CapsuleWorkerSemanticStatus(TypedDict):
|
210
|
-
final_version: str
|
211
|
-
status: CapsuleWorkerStatusDict
|
212
|
-
worker_info: WorkerInfoDict
|
213
|
-
|
214
|
-
|
215
|
-
def _capsule_worker_status_diff(
|
216
|
-
current_status: CapsuleWorkerSemanticStatus,
|
217
|
-
previous_status: Union[CapsuleWorkerSemanticStatus, None],
|
218
|
-
) -> List[str]:
|
219
|
-
"""
|
220
|
-
The goal of this function is to return a status string that will be used to update the user the
|
221
|
-
change in status of the different capsules.
|
222
|
-
"""
|
223
|
-
if previous_status is None:
|
224
|
-
# Check if the current status has pending workers or crashlooping workers
|
225
|
-
curr = current_status["status"]["current_info"]
|
226
|
-
version = current_status["final_version"]
|
227
|
-
changes = []
|
228
|
-
|
229
|
-
if curr["pending"] > 0:
|
230
|
-
changes.append(f"⏳ {curr['pending']} worker(s) pending")
|
231
|
-
|
232
|
-
if curr["running"] > 0:
|
233
|
-
changes.append(f"🚀 {curr['running']} worker(s) currently running")
|
234
|
-
|
235
|
-
if curr["crashlooping"] > 0:
|
236
|
-
changes.append(f"💥 {curr['crashlooping']} worker(s) currently crashlooping")
|
237
|
-
|
238
|
-
return changes
|
239
|
-
|
240
|
-
curr = current_status["status"]["current_info"]
|
241
|
-
prev = previous_status["status"]["current_info"]
|
242
|
-
version = current_status["final_version"]
|
243
|
-
|
244
|
-
changes = []
|
245
|
-
|
246
|
-
# Track worker count changes for the target version
|
247
|
-
pending_diff = curr["pending"] - prev["pending"]
|
248
|
-
running_diff = curr["running"] - prev["running"]
|
249
|
-
crash_diff = curr["crashlooping"] - prev["crashlooping"]
|
250
|
-
|
251
|
-
# Worker count changes
|
252
|
-
if pending_diff > 0:
|
253
|
-
changes.append(
|
254
|
-
f"⏳ {pending_diff} new worker(s) pending. Total pending ({curr['pending']})"
|
255
|
-
)
|
256
|
-
|
257
|
-
if running_diff > 0:
|
258
|
-
changes.append(
|
259
|
-
f"🚀 {running_diff} worker(s) started running. Total running ({curr['running']})"
|
260
|
-
)
|
261
|
-
elif running_diff < 0:
|
262
|
-
changes.append(
|
263
|
-
f"🛑 {abs(running_diff)} worker(s) stopped running. Total running ({curr['running']})"
|
264
|
-
)
|
265
|
-
|
266
|
-
if crash_diff > 0:
|
267
|
-
changes.append(
|
268
|
-
f"💥 {crash_diff} worker(s) started crashlooping. Total crashlooping ({curr['crashlooping']})"
|
269
|
-
)
|
270
|
-
elif crash_diff < 0:
|
271
|
-
changes.append(f"🔧 {abs(crash_diff)} worker(s) recovered from crashlooping")
|
272
|
-
|
273
|
-
# Significant state transitions
|
274
|
-
if (
|
275
|
-
not previous_status["status"]["at_least_one_running"]
|
276
|
-
and current_status["status"]["at_least_one_running"]
|
277
|
-
):
|
278
|
-
changes.append(f"✅ First worker came online")
|
279
|
-
|
280
|
-
if (
|
281
|
-
not previous_status["status"]["all_running"]
|
282
|
-
and current_status["status"]["all_running"]
|
283
|
-
):
|
284
|
-
changes.append(f"🎉 All workers are now running")
|
285
|
-
|
286
|
-
if (
|
287
|
-
not previous_status["status"]["at_least_one_crashlooping"]
|
288
|
-
and current_status["status"]["at_least_one_crashlooping"]
|
289
|
-
):
|
290
|
-
changes.append(f"⚠️ Worker crash detected")
|
291
|
-
|
292
|
-
# Current state summary
|
293
|
-
|
294
|
-
return changes
|
295
|
-
|
296
|
-
|
297
|
-
def _capsule_worker_semantic_status(
|
298
|
-
workers: List[WorkerStatus], version: str, min_replicas: int
|
299
|
-
) -> CapsuleWorkerSemanticStatus:
|
300
|
-
def _make_version_dict(
|
301
|
-
_workers: List[WorkerStatus], phase: str
|
302
|
-
) -> Dict[str, List[WorkerStatus]]:
|
303
|
-
xx: Dict[str, List[WorkerStatus]] = {}
|
304
|
-
for w in _workers:
|
305
|
-
if w.get("phase") != phase:
|
306
|
-
continue
|
307
|
-
if w.get("version") not in xx:
|
308
|
-
xx[w.get("version")] = []
|
309
|
-
xx[w.get("version")].append(w)
|
310
|
-
return xx
|
311
|
-
|
312
|
-
pending_workers = _make_version_dict(workers, "Pending")
|
313
|
-
running_workers = _make_version_dict(workers, "Running")
|
314
|
-
crashlooping_workers = _make_version_dict(workers, "CrashLoopBackOff")
|
315
|
-
|
316
|
-
# current_status (formulated basis):
|
317
|
-
# - atleast one pods are pending for `_end_state_capsule_version`
|
318
|
-
# - atleast one pod is in Running state for `_end_state_capsule_version` (maybe terminal) [Might require heath-check thing here]
|
319
|
-
# - alteast one pod is crashlooping for `_end_state_capsule_version` (maybe terminal)
|
320
|
-
# - all pods are running for `_end_state_capsule_version` that match the minimum number of replicas
|
321
|
-
# - all pods are running for `_end_state_capsule_version` that match the maximum number of replicas and no other pods of older versions are running
|
322
|
-
# - no pods relating to `_end_state_capsule_version` are pending/running/crashlooping
|
323
|
-
|
324
|
-
# Helper to count pods for the final version in each state
|
325
|
-
def count_for_version(workers_dict):
|
326
|
-
return len(workers_dict.get(version, []))
|
327
|
-
|
328
|
-
status_dict: CapsuleWorkerStatusDict = {
|
329
|
-
"at_least_one_pending": count_for_version(pending_workers) > 0,
|
330
|
-
"at_least_one_running": count_for_version(running_workers) > 0,
|
331
|
-
"at_least_one_crashlooping": count_for_version(crashlooping_workers) > 0,
|
332
|
-
"none_present": (
|
333
|
-
count_for_version(running_workers) == 0
|
334
|
-
and count_for_version(pending_workers) == 0
|
335
|
-
and count_for_version(crashlooping_workers) == 0
|
336
|
-
),
|
337
|
-
"all_running": count_for_version(running_workers) == min_replicas,
|
338
|
-
"fully_finished": count_for_version(running_workers) == min_replicas
|
339
|
-
and len(pending_workers) == 0
|
340
|
-
and len(crashlooping_workers) == 0,
|
341
|
-
"current_info": {
|
342
|
-
"pending": count_for_version(pending_workers),
|
343
|
-
"running": count_for_version(running_workers),
|
344
|
-
"crashlooping": count_for_version(crashlooping_workers),
|
345
|
-
},
|
346
|
-
}
|
347
|
-
|
348
|
-
worker_info: WorkerInfoDict = {
|
349
|
-
"pending": pending_workers,
|
350
|
-
"running": running_workers,
|
351
|
-
"crashlooping": crashlooping_workers,
|
352
|
-
}
|
353
|
-
|
354
|
-
return {
|
355
|
-
"final_version": version,
|
356
|
-
"status": status_dict,
|
357
|
-
"worker_info": worker_info,
|
358
|
-
}
|