rdeploy 0.2.0__tar.gz → 0.4.0__tar.gz
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.
- {rdeploy-0.2.0 → rdeploy-0.4.0}/CHANGES.txt +30 -0
- {rdeploy-0.2.0/rdeploy.egg-info → rdeploy-0.4.0}/PKG-INFO +2 -2
- {rdeploy-0.2.0 → rdeploy-0.4.0}/README.md +1 -1
- rdeploy-0.4.0/VERSION +1 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/rdeploy/tasks.py +255 -12
- {rdeploy-0.2.0 → rdeploy-0.4.0/rdeploy.egg-info}/PKG-INFO +2 -2
- rdeploy-0.4.0/tests/test_tasks.py +669 -0
- rdeploy-0.2.0/VERSION +0 -1
- rdeploy-0.2.0/tests/test_tasks.py +0 -299
- {rdeploy-0.2.0 → rdeploy-0.4.0}/LICENSE +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/pyproject.toml +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/rdeploy/__init__.py +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/rdeploy/exceptions.py +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/rdeploy/main.py +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/rdeploy/utils.py +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/rdeploy.egg-info/SOURCES.txt +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/rdeploy.egg-info/dependency_links.txt +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/rdeploy.egg-info/entry_points.txt +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/rdeploy.egg-info/requires.txt +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/rdeploy.egg-info/top_level.txt +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/requirements.txt +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/setup.cfg +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/tests/test_exceptions.py +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/tests/test_main.py +0 -0
- {rdeploy-0.2.0 → rdeploy-0.4.0}/tests/test_utils.py +0 -0
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
----
|
|
2
|
+
Version 0.4.0, Fri 10 Jul 2026
|
|
3
|
+
===============================
|
|
4
|
+
- `rdeploy upgrade` now shows live rollout progress while the atomic helm
|
|
5
|
+
upgrade waits: it polls `kubectl get pods` and shows the pod status table
|
|
6
|
+
(prefixed with `[rollout]`), so you can watch pods being created, pulled,
|
|
7
|
+
started and terminated (including helm hook pods such as migration jobs)
|
|
8
|
+
instead of blocking silently. On a terminal the table is pinned to the
|
|
9
|
+
bottom of the screen and updates in place while helm's output scrolls
|
|
10
|
+
above it; on a non-TTY (e.g. CI logs) it prints the table once, then any
|
|
11
|
+
row that changes.
|
|
12
|
+
- Added `--no-progress` to disable the live progress display. The display is
|
|
13
|
+
purely informational and runs concurrently with helm; helm `--atomic`
|
|
14
|
+
remains the sole arbiter of success, failure and rollback, and the watcher
|
|
15
|
+
never affects the command's exit code.
|
|
16
|
+
|
|
17
|
+
----
|
|
18
|
+
Version 0.3.0, Thu 09 Jul 2026
|
|
19
|
+
===============================
|
|
20
|
+
- `rdeploy upgrade` now runs helm with `--atomic` by default: helm waits for
|
|
21
|
+
the rollout to complete (up to `--timeout`, default 15m) and automatically
|
|
22
|
+
rolls back to the previous release if it fails, and the command exits with
|
|
23
|
+
an error instead of reporting success.
|
|
24
|
+
- Added `--no-atomic` to restore the previous fire-and-forget behaviour and
|
|
25
|
+
`--timeout` to adjust how long helm waits.
|
|
26
|
+
|
|
27
|
+
BREAKING CHANGES:
|
|
28
|
+
- `rdeploy upgrade` now blocks until the rollout completes and exits non-zero
|
|
29
|
+
on failure. Scripts that relied on it returning immediately should pass
|
|
30
|
+
`--no-atomic`.
|
|
31
|
+
|
|
2
32
|
Version 0.2.0, Thu 09 Jul 2026
|
|
3
33
|
===============================
|
|
4
34
|
- Upgraded all dependencies to current versions: invoke 2.2+/3.x, semver 3.x,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rdeploy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: CLI that makes it easy to build and deploy an application to k8s.
|
|
5
5
|
Author-email: Rehive <info@rehive.com>
|
|
6
6
|
License: MIT
|
|
@@ -73,7 +73,7 @@ rdeploy git-release patch
|
|
|
73
73
|
| `set-context` | Switch cluster and namespace context |
|
|
74
74
|
| `activate` | Fetch and set project, cluster, and namespace |
|
|
75
75
|
| `install` | Install a Helm chart deployment |
|
|
76
|
-
| `upgrade` | Upgrade a Helm deployment to a new image tag |
|
|
76
|
+
| `upgrade` | Upgrade a Helm deployment to a new image tag (atomic by default: waits for the rollout and rolls back on failure; use `--no-atomic` to skip, `--timeout` to adjust the wait, default `15m`) |
|
|
77
77
|
| `helm` | Run arbitrary Helm commands |
|
|
78
78
|
| `helm-setup` | Download and configure a local Helm binary |
|
|
79
79
|
| `git-release` | Bump version and push a git tag |
|
|
@@ -36,7 +36,7 @@ rdeploy git-release patch
|
|
|
36
36
|
| `set-context` | Switch cluster and namespace context |
|
|
37
37
|
| `activate` | Fetch and set project, cluster, and namespace |
|
|
38
38
|
| `install` | Install a Helm chart deployment |
|
|
39
|
-
| `upgrade` | Upgrade a Helm deployment to a new image tag |
|
|
39
|
+
| `upgrade` | Upgrade a Helm deployment to a new image tag (atomic by default: waits for the rollout and rolls back on failure; use `--no-atomic` to skip, `--timeout` to adjust the wait, default `15m`) |
|
|
40
40
|
| `helm` | Run arbitrary Helm commands |
|
|
41
41
|
| `helm-setup` | Download and configure a local Helm binary |
|
|
42
42
|
| `git-release` | Bump version and push a git tag |
|
rdeploy-0.4.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.4.0
|
|
@@ -3,6 +3,10 @@ import tarfile
|
|
|
3
3
|
import zipfile
|
|
4
4
|
import urllib.request
|
|
5
5
|
import io
|
|
6
|
+
import shutil
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import subprocess
|
|
6
10
|
|
|
7
11
|
from invoke import task
|
|
8
12
|
import semver
|
|
@@ -370,10 +374,194 @@ def install(ctx, config):
|
|
|
370
374
|
echo=True)
|
|
371
375
|
|
|
372
376
|
|
|
377
|
+
class _RolloutDisplay:
|
|
378
|
+
"""Pin a live status region to the bottom of a TTY while other output
|
|
379
|
+
scrolls above it.
|
|
380
|
+
|
|
381
|
+
The cursor arithmetic only stays consistent if every terminal write goes
|
|
382
|
+
through this object, so helm's stdout/stderr are routed here as well (via
|
|
383
|
+
invoke's out_stream/err_stream) and rendered above the pinned region.
|
|
384
|
+
Uses plain ANSI: ESC[nA moves the cursor up n lines, ESC[K clears to the
|
|
385
|
+
end of the line, ESC[J clears to the end of the screen.
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
def __init__(self, stream):
|
|
389
|
+
self._stream = stream
|
|
390
|
+
self._lock = threading.Lock()
|
|
391
|
+
self._region = []
|
|
392
|
+
self._drawn = 0
|
|
393
|
+
|
|
394
|
+
def log(self, text):
|
|
395
|
+
"""Write scrolling output (complete lines) above the status region."""
|
|
396
|
+
with self._lock:
|
|
397
|
+
self._render(log_lines=text.splitlines())
|
|
398
|
+
|
|
399
|
+
def update(self, lines):
|
|
400
|
+
"""Replace the pinned status region with the given lines."""
|
|
401
|
+
with self._lock:
|
|
402
|
+
self._region = list(lines)
|
|
403
|
+
self._render()
|
|
404
|
+
|
|
405
|
+
def _render(self, log_lines=()):
|
|
406
|
+
# Cursor rests at column 0 just below the region; move to the
|
|
407
|
+
# region's top, write the scrolling lines (overwriting old region
|
|
408
|
+
# rows), redraw the region beneath them, and clear any leftover rows
|
|
409
|
+
# from a previously taller region.
|
|
410
|
+
if self._drawn:
|
|
411
|
+
self._stream.write('\x1b[{n}A'.format(n=self._drawn))
|
|
412
|
+
for line in log_lines:
|
|
413
|
+
self._stream.write(line + '\x1b[K\n')
|
|
414
|
+
# Region lines must not wrap or the cursor-up count breaks, so
|
|
415
|
+
# truncate them to the terminal width. Scrolling lines may wrap
|
|
416
|
+
# freely: they end up above the region either way.
|
|
417
|
+
width = shutil.get_terminal_size((120, 24)).columns
|
|
418
|
+
for line in self._region:
|
|
419
|
+
self._stream.write(line[:width - 1] + '\x1b[K\n')
|
|
420
|
+
self._stream.write('\x1b[J')
|
|
421
|
+
self._stream.flush()
|
|
422
|
+
self._drawn = len(self._region)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class _LogStream(io.TextIOBase):
|
|
426
|
+
"""File-like stream for invoke's out_stream/err_stream that forwards
|
|
427
|
+
complete lines to a _RolloutDisplay, buffering partial lines so the
|
|
428
|
+
display's line accounting stays exact."""
|
|
429
|
+
|
|
430
|
+
def __init__(self, display):
|
|
431
|
+
self._display = display
|
|
432
|
+
self._buf = ''
|
|
433
|
+
|
|
434
|
+
def write(self, s):
|
|
435
|
+
self._buf += s
|
|
436
|
+
if '\n' in self._buf:
|
|
437
|
+
complete, self._buf = self._buf.rsplit('\n', 1)
|
|
438
|
+
self._display.log(complete + '\n')
|
|
439
|
+
return len(s)
|
|
440
|
+
|
|
441
|
+
def flush(self):
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
def drain(self):
|
|
445
|
+
"""Emit any trailing partial line (call once the command is done)."""
|
|
446
|
+
if self._buf:
|
|
447
|
+
self._display.log(self._buf + '\n')
|
|
448
|
+
self._buf = ''
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _watch_rollout(stop_event, poll_interval=2, display=None):
|
|
452
|
+
"""Show live `kubectl get pods` progress while the atomic helm upgrade
|
|
453
|
+
waits, so you can see pods being created, pulled, started and terminated
|
|
454
|
+
instead of a silent wait.
|
|
455
|
+
|
|
456
|
+
Pods (not deployments) are polled because for single-replica services the
|
|
457
|
+
deployment counters are blind to almost the whole rollout: while the new
|
|
458
|
+
pod is created and pulls its image the row reads 1/1, identical to steady
|
|
459
|
+
state, and only blinks to 2/1 for the second or two between the new pod
|
|
460
|
+
becoming ready and the old one being deleted. Pod rows change visibly for
|
|
461
|
+
the entire rollout (ContainerCreating during the pull, Running,
|
|
462
|
+
Terminating, CrashLoopBackOff on a bad release) and also surface helm
|
|
463
|
+
hook pods such as migration jobs.
|
|
464
|
+
|
|
465
|
+
With a display (stdout is a TTY) the whole pod table is pinned to the
|
|
466
|
+
bottom of the terminal and updated in place every poll, while helm's own
|
|
467
|
+
output scrolls above it. Without one (non-TTY, e.g. CI logs) it prints
|
|
468
|
+
append-only lines: the table once as a baseline, then only rows whose
|
|
469
|
+
READY/STATUS/RESTARTS change (ignoring the ticking AGE and restart-age
|
|
470
|
+
columns).
|
|
471
|
+
|
|
472
|
+
Runs in a background daemon thread and is fully decoupled from the
|
|
473
|
+
upgrade: its output is informational only and its exit codes are ignored.
|
|
474
|
+
Every poll reflects the cluster's current state -- pods created by the
|
|
475
|
+
upgrade itself show up automatically, and there are no long-lived child
|
|
476
|
+
processes to tear down: each poll is one short, bounded `kubectl get`.
|
|
477
|
+
|
|
478
|
+
set_context() has already pointed kubectl at the right context and
|
|
479
|
+
namespace. The whole body is wrapped in a broad except so a broken
|
|
480
|
+
watcher (kubectl missing, permission error) can never raise into the
|
|
481
|
+
main flow or change the exit code.
|
|
482
|
+
"""
|
|
483
|
+
last_rows = {}
|
|
484
|
+
header_printed = False
|
|
485
|
+
failures = 0
|
|
486
|
+
|
|
487
|
+
def _note(message):
|
|
488
|
+
line = '[rollout] {message}'.format(message=message)
|
|
489
|
+
if display is not None:
|
|
490
|
+
display.update([])
|
|
491
|
+
display.log(line + '\n')
|
|
492
|
+
else:
|
|
493
|
+
print(line, flush=True)
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
while not stop_event.is_set():
|
|
497
|
+
status = subprocess.run(['kubectl', 'get', 'pods',
|
|
498
|
+
'--request-timeout=10s'],
|
|
499
|
+
capture_output=True, text=True)
|
|
500
|
+
if status.returncode == 0:
|
|
501
|
+
failures = 0
|
|
502
|
+
lines = status.stdout.splitlines()
|
|
503
|
+
if display is not None:
|
|
504
|
+
# Live region: redraw the whole current table in place.
|
|
505
|
+
region = ['[rollout] {line}'.format(line=line)
|
|
506
|
+
for line in lines]
|
|
507
|
+
if region:
|
|
508
|
+
region.append('[rollout] (live view, updated {ts})'
|
|
509
|
+
.format(ts=time.strftime('%H:%M:%S')))
|
|
510
|
+
display.update(region)
|
|
511
|
+
else:
|
|
512
|
+
# Append-only: baseline table once, then changed rows.
|
|
513
|
+
to_print = []
|
|
514
|
+
if lines and not header_printed:
|
|
515
|
+
header_printed = True
|
|
516
|
+
to_print.append(lines[0])
|
|
517
|
+
for row in lines[1:]:
|
|
518
|
+
fields = row.split()
|
|
519
|
+
if not fields:
|
|
520
|
+
continue
|
|
521
|
+
# Compare only NAME READY STATUS RESTARTS so the
|
|
522
|
+
# ticking AGE and the '(Xm ago)' restart annotation
|
|
523
|
+
# don't reprint an otherwise unchanged row.
|
|
524
|
+
name, key = fields[0], ' '.join(fields[:4])
|
|
525
|
+
if last_rows.get(name) != key:
|
|
526
|
+
last_rows[name] = key
|
|
527
|
+
to_print.append(row)
|
|
528
|
+
for line in to_print:
|
|
529
|
+
print('[rollout] {line}'.format(line=line),
|
|
530
|
+
flush=True)
|
|
531
|
+
else:
|
|
532
|
+
# Tolerate transient API errors, but give up (quietly, with a
|
|
533
|
+
# single note) if kubectl keeps failing.
|
|
534
|
+
failures += 1
|
|
535
|
+
if failures >= 3:
|
|
536
|
+
error = (status.stderr or '').strip().splitlines()
|
|
537
|
+
_note('progress display unavailable: {err}'
|
|
538
|
+
.format(err=error[0] if error else
|
|
539
|
+
'kubectl exited with code {rc}'
|
|
540
|
+
.format(rc=status.returncode)))
|
|
541
|
+
return
|
|
542
|
+
stop_event.wait(poll_interval)
|
|
543
|
+
except Exception as err:
|
|
544
|
+
_note('progress display unavailable: {err}'.format(err=err))
|
|
545
|
+
|
|
546
|
+
|
|
373
547
|
@task
|
|
374
|
-
def upgrade(ctx, config, tag):
|
|
548
|
+
def upgrade(ctx, config, tag, atomic=True, timeout='15m', progress=True):
|
|
375
549
|
"""
|
|
376
550
|
Upgrades kubernetes deployment
|
|
551
|
+
|
|
552
|
+
By default the upgrade runs atomically: helm waits for the rollout to
|
|
553
|
+
complete and rolls back to the previous release if it fails, and the
|
|
554
|
+
task exits with an error. Use --no-atomic to restore the old
|
|
555
|
+
fire-and-forget behaviour, and --timeout to change how long helm waits.
|
|
556
|
+
|
|
557
|
+
While the atomic upgrade waits, a live progress display polls
|
|
558
|
+
`kubectl get pods` and shows the pod status table (prefixed with
|
|
559
|
+
[rollout]), so you can watch pods being created, pulled, started and
|
|
560
|
+
terminated instead of a silent wait. On a terminal the table is pinned
|
|
561
|
+
to the bottom of the screen and updates in place while helm's output
|
|
562
|
+
scrolls above it; on a non-TTY it prints the table once, then any row
|
|
563
|
+
that changes. This is purely informational and never affects the
|
|
564
|
+
result; use --no-progress to disable it.
|
|
377
565
|
"""
|
|
378
566
|
|
|
379
567
|
settings_dict = get_settings()
|
|
@@ -391,17 +579,72 @@ def upgrade(ctx, config, tag):
|
|
|
391
579
|
else:
|
|
392
580
|
helm_chart = config_dict['helm_chart']
|
|
393
581
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
582
|
+
atomic_flags = ' --atomic --timeout {timeout}'.format(timeout=timeout) if atomic else ''
|
|
583
|
+
|
|
584
|
+
# Only watch when there is an atomic wait to watch (--no-atomic returns
|
|
585
|
+
# immediately) and progress display is enabled.
|
|
586
|
+
show_progress = atomic and progress
|
|
587
|
+
stop_event = threading.Event()
|
|
588
|
+
watcher = None
|
|
589
|
+
display = None
|
|
590
|
+
run_streams = {}
|
|
591
|
+
if show_progress:
|
|
592
|
+
if sys.stdout.isatty():
|
|
593
|
+
# Live view: the pod table is pinned to the bottom of the
|
|
594
|
+
# terminal and helm's output scrolls above it. Helm's streams
|
|
595
|
+
# must be routed through the display for the cursor arithmetic
|
|
596
|
+
# to hold, so hand invoke our own out/err streams.
|
|
597
|
+
display = _RolloutDisplay(sys.stdout)
|
|
598
|
+
run_streams = {'out_stream': _LogStream(display),
|
|
599
|
+
'err_stream': _LogStream(display)}
|
|
600
|
+
watcher = threading.Thread(target=_watch_rollout, args=(stop_event,),
|
|
601
|
+
kwargs={'display': display}, daemon=True)
|
|
602
|
+
|
|
603
|
+
helm_command = ('{helm_bin} upgrade {project_name} '
|
|
604
|
+
'--values {helm_values_path} '
|
|
605
|
+
'--set image.tag={version} '
|
|
606
|
+
'--version {helm_chart_version}{atomic_flags} {helm_chart}'
|
|
607
|
+
.format(helm_bin=helm_bin,
|
|
608
|
+
project_name=config_dict['project_name'],
|
|
609
|
+
helm_chart=helm_chart,
|
|
610
|
+
helm_values_path=config_dict['helm_values_path'],
|
|
611
|
+
version=tag,
|
|
612
|
+
helm_chart_version=config_dict['helm_chart_version'],
|
|
613
|
+
atomic_flags=atomic_flags))
|
|
614
|
+
|
|
615
|
+
# The helm call below is the sole source of truth for success/failure/
|
|
616
|
+
# rollback. The watcher runs concurrently and is stopped in finally
|
|
617
|
+
# whether helm succeeds or rolls back; its output never influences result.
|
|
618
|
+
try:
|
|
619
|
+
if watcher is not None:
|
|
620
|
+
watcher.start()
|
|
621
|
+
|
|
622
|
+
if display is not None:
|
|
623
|
+
# invoke's echo writes straight to stdout, which would race with
|
|
624
|
+
# the display's cursor arithmetic; echo through the display
|
|
625
|
+
# instead (same bold formatting as invoke).
|
|
626
|
+
display.log('\033[1;37m{cmd}\033[0m\n'.format(cmd=helm_command))
|
|
627
|
+
result = ctx.run(helm_command, echo=display is None, warn=True,
|
|
628
|
+
**run_streams)
|
|
629
|
+
finally:
|
|
630
|
+
stop_event.set()
|
|
631
|
+
if watcher is not None:
|
|
632
|
+
# A poll in flight finishes within kubectl's --request-timeout,
|
|
633
|
+
# so this join is bounded and leaves no child processes behind.
|
|
634
|
+
watcher.join(timeout=15)
|
|
635
|
+
for stream in run_streams.values():
|
|
636
|
+
stream.drain()
|
|
637
|
+
|
|
638
|
+
if result.failed:
|
|
639
|
+
if atomic:
|
|
640
|
+
sys.exit('Upgrade of {project_name} to {version} failed.'
|
|
641
|
+
' Helm rolled the release back to the previous version.'
|
|
642
|
+
.format(project_name=config_dict['project_name'],
|
|
643
|
+
version=tag))
|
|
644
|
+
else:
|
|
645
|
+
sys.exit('Upgrade of {project_name} to {version} failed.'
|
|
646
|
+
.format(project_name=config_dict['project_name'],
|
|
647
|
+
version=tag))
|
|
405
648
|
|
|
406
649
|
|
|
407
650
|
@task
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rdeploy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: CLI that makes it easy to build and deploy an application to k8s.
|
|
5
5
|
Author-email: Rehive <info@rehive.com>
|
|
6
6
|
License: MIT
|
|
@@ -73,7 +73,7 @@ rdeploy git-release patch
|
|
|
73
73
|
| `set-context` | Switch cluster and namespace context |
|
|
74
74
|
| `activate` | Fetch and set project, cluster, and namespace |
|
|
75
75
|
| `install` | Install a Helm chart deployment |
|
|
76
|
-
| `upgrade` | Upgrade a Helm deployment to a new image tag |
|
|
76
|
+
| `upgrade` | Upgrade a Helm deployment to a new image tag (atomic by default: waits for the rollout and rolls back on failure; use `--no-atomic` to skip, `--timeout` to adjust the wait, default `15m`) |
|
|
77
77
|
| `helm` | Run arbitrary Helm commands |
|
|
78
78
|
| `helm-setup` | Download and configure a local Helm binary |
|
|
79
79
|
| `git-release` | Bump version and push a git tag |
|