squarepeg 0.4.0__tar.gz → 0.4.1__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.
Files changed (54) hide show
  1. {squarepeg-0.4.0 → squarepeg-0.4.1}/PKG-INFO +4 -1
  2. {squarepeg-0.4.0 → squarepeg-0.4.1}/README.md +3 -0
  3. {squarepeg-0.4.0 → squarepeg-0.4.1}/pyproject.toml +1 -1
  4. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/cli.py +5 -3
  5. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/config.py +1 -0
  6. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/k8s/session.py +23 -3
  7. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg.egg-info/PKG-INFO +4 -1
  8. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_cli.py +35 -12
  9. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_session.py +24 -0
  10. {squarepeg-0.4.0 → squarepeg-0.4.1}/setup.cfg +0 -0
  11. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/__init__.py +0 -0
  12. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/__main__.py +0 -0
  13. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/dockerargs.py +0 -0
  14. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/env_interpolation.py +0 -0
  15. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/errors.py +0 -0
  16. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/k8s/__init__.py +0 -0
  17. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/k8s/apierrors.py +0 -0
  18. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/k8s/dryrun.py +0 -0
  19. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/k8s/events.py +0 -0
  20. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/k8s/logs.py +0 -0
  21. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/k8s/orphans.py +0 -0
  22. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/k8s/podstate.py +0 -0
  23. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/k8s/runner.py +0 -0
  24. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/labels.py +0 -0
  25. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/log.py +0 -0
  26. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/manifest.py +0 -0
  27. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/merge.py +0 -0
  28. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/naming.py +0 -0
  29. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/quantities.py +0 -0
  30. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/runspec.py +0 -0
  31. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/ui.py +0 -0
  32. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg/volumes.py +0 -0
  33. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg.egg-info/SOURCES.txt +0 -0
  34. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg.egg-info/dependency_links.txt +0 -0
  35. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg.egg-info/entry_points.txt +0 -0
  36. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg.egg-info/requires.txt +0 -0
  37. {squarepeg-0.4.0 → squarepeg-0.4.1}/squarepeg.egg-info/top_level.txt +0 -0
  38. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_apierrors.py +0 -0
  39. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_config.py +0 -0
  40. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_dockerargs.py +0 -0
  41. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_dryrun.py +0 -0
  42. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_env_interpolation.py +0 -0
  43. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_events.py +0 -0
  44. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_integration.py +0 -0
  45. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_logs.py +0 -0
  46. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_manifest.py +0 -0
  47. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_merge.py +0 -0
  48. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_naming.py +0 -0
  49. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_orphans.py +0 -0
  50. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_podstate.py +0 -0
  51. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_quantities.py +0 -0
  52. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_runner.py +0 -0
  53. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_ui.py +0 -0
  54. {squarepeg-0.4.0 → squarepeg-0.4.1}/tests/test_volumes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: squarepeg
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Run a docker-run-style command as a Kubernetes Pod or Job, streaming its output like a local process
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -49,6 +49,9 @@ Full documentation lives in the [wiki](https://github.com/BioWilko/squarepeg/wik
49
49
  - [Troubleshooting](https://github.com/BioWilko/squarepeg/wiki/Troubleshooting)
50
50
  - [Development](https://github.com/BioWilko/squarepeg/wiki/Development)
51
51
 
52
+ > **MicroK8s on Python 3.13+:** if connecting fails with `CA cert does not include key usage extension`,
53
+ > set `lenient_tls: true` in your config — see [Troubleshooting](https://github.com/BioWilko/squarepeg/wiki/Troubleshooting#runner-errors-exit-code-125).
54
+
52
55
  See also [`examples/config.yaml`](examples/config.yaml) for a fully worked
53
56
  config file.
54
57
 
@@ -35,6 +35,9 @@ Full documentation lives in the [wiki](https://github.com/BioWilko/squarepeg/wik
35
35
  - [Troubleshooting](https://github.com/BioWilko/squarepeg/wiki/Troubleshooting)
36
36
  - [Development](https://github.com/BioWilko/squarepeg/wiki/Development)
37
37
 
38
+ > **MicroK8s on Python 3.13+:** if connecting fails with `CA cert does not include key usage extension`,
39
+ > set `lenient_tls: true` in your config — see [Troubleshooting](https://github.com/BioWilko/squarepeg/wiki/Troubleshooting#runner-errors-exit-code-125).
40
+
38
41
  See also [`examples/config.yaml`](examples/config.yaml) for a fully worked
39
42
  config file.
40
43
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "squarepeg"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "Run a docker-run-style command as a Kubernetes Pod or Job, streaming its output like a local process"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -354,6 +354,7 @@ def run(
354
354
  else coerce_int(resolved_config.get("timeout", 300), "timeout")
355
355
  )
356
356
  quiet = quiet or coerce_bool(resolved_config.get("quiet", False), "quiet")
357
+ lenient_tls = coerce_bool(resolved_config.get("lenient_tls", False), "lenient_tls")
357
358
  cleanup = (
358
359
  False if keep else coerce_bool(resolved_config.get("cleanup", True), "cleanup")
359
360
  )
@@ -435,7 +436,7 @@ def run(
435
436
  ui.set_run_tag(effective_run_id)
436
437
 
437
438
  if dry_run_server:
438
- session = Session(namespace=spec.namespace, context=context, quiet=spec.quiet)
439
+ session = Session(namespace=spec.namespace, context=context, quiet=spec.quiet, lenient_tls=lenient_tls)
439
440
  spec.namespace = session.namespace
440
441
  validated = server_dry_run(session, spec, manifest)
441
442
  click.echo(yaml.safe_dump(validated, sort_keys=False), nl=False)
@@ -444,7 +445,7 @@ def run(
444
445
  click.echo(yaml.safe_dump(manifest, sort_keys=False), nl=False)
445
446
  return
446
447
 
447
- session = Session(namespace=spec.namespace, context=context, quiet=spec.quiet)
448
+ session = Session(namespace=spec.namespace, context=context, quiet=spec.quiet, lenient_tls=lenient_tls)
448
449
  spec.namespace = session.namespace
449
450
  exit_code = run_manifest(session, spec, manifest)
450
451
  raise SystemExit(exit_code)
@@ -510,6 +511,7 @@ def clean(dry_run, min_age, namespace, context, quiet, no_color, config_paths, n
510
511
  if namespace is None:
511
512
  namespace = resolved_config.get("namespace")
512
513
  quiet = quiet or coerce_bool(resolved_config.get("quiet", False), "quiet")
514
+ lenient_tls = coerce_bool(resolved_config.get("lenient_tls", False), "lenient_tls")
513
515
 
514
516
  if min_age is None:
515
517
  min_age = coerce_int(
@@ -520,7 +522,7 @@ def clean(dry_run, min_age, namespace, context, quiet, no_color, config_paths, n
520
522
  elif min_age < 0:
521
523
  raise UsageError(f"--min-age must not be negative, got {min_age}")
522
524
 
523
- session = Session(namespace=namespace, context=context, quiet=quiet)
525
+ session = Session(namespace=namespace, context=context, quiet=quiet, lenient_tls=lenient_tls)
524
526
 
525
527
  with ui.Status("scanning for orphaned resources", quiet=quiet):
526
528
  result = clean_orphans(session, min_age, quiet=quiet, dry_run=dry_run)
@@ -31,6 +31,7 @@ ALLOWED_TOP_KEYS = {
31
31
  "timeout",
32
32
  "quiet",
33
33
  "allow_host_path_mounts",
34
+ "lenient_tls",
34
35
  "split_streams",
35
36
  "orphan_sweep",
36
37
  "orphan_sweep_min_age",
@@ -1,8 +1,10 @@
1
+ import ssl
1
2
  from pathlib import Path
2
3
 
3
4
  from kubernetes import client
4
5
  from kubernetes import config as kube_config
5
6
  from kubernetes.config.config_exception import ConfigException
7
+ from urllib3.util.ssl_ import create_urllib3_context
6
8
 
7
9
  from squarepeg.errors import RunnerError
8
10
  from squarepeg.log import chatter
@@ -47,7 +49,13 @@ class Session:
47
49
  surface that failure, not be silently masked by an in-cluster fallback.
48
50
  """
49
51
 
50
- def __init__(self, namespace: str | None = None, context: str | None = None, quiet: bool = False):
52
+ def __init__(
53
+ self,
54
+ namespace: str | None = None,
55
+ context: str | None = None,
56
+ quiet: bool = False,
57
+ lenient_tls: bool = False,
58
+ ):
51
59
  in_cluster = False
52
60
  try:
53
61
  kube_config.load_kube_config(context=context)
@@ -73,7 +81,19 @@ class Session:
73
81
 
74
82
  self.namespace = namespace or context_namespace or "default"
75
83
 
76
- self.core = client.CoreV1Api()
77
- self.batch = client.BatchV1Api()
84
+ api_client = None
85
+ if lenient_tls:
86
+ # short-term workaround for https://github.com/canonical/microk8s/issues/4864: microk8s's CA
87
+ # lacks the keyUsage extension, which Python 3.13+/urllib3 2.3+ reject via VERIFY_X509_STRICT.
88
+ # Only that flag is cleared -- CERT_REQUIRED, CA chain and hostname checks are all still
89
+ # enforced. The kubernetes client exposes no ssl_context option, so it's injected into the
90
+ # (lazily created) urllib3 connection pools' kwargs instead.
91
+ ssl_context = create_urllib3_context()
92
+ ssl_context.verify_flags &= ~ssl.VERIFY_X509_STRICT
93
+ api_client = client.ApiClient()
94
+ api_client.rest_client.pool_manager.connection_pool_kw["ssl_context"] = ssl_context
95
+
96
+ self.core = client.CoreV1Api(api_client)
97
+ self.batch = client.BatchV1Api(api_client)
78
98
 
79
99
  chatter(f"using context {self.context_name!r}, namespace {self.namespace!r}", quiet=quiet)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: squarepeg
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Run a docker-run-style command as a Kubernetes Pod or Job, streaming its output like a local process
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -49,6 +49,9 @@ Full documentation lives in the [wiki](https://github.com/BioWilko/squarepeg/wik
49
49
  - [Troubleshooting](https://github.com/BioWilko/squarepeg/wiki/Troubleshooting)
50
50
  - [Development](https://github.com/BioWilko/squarepeg/wiki/Development)
51
51
 
52
+ > **MicroK8s on Python 3.13+:** if connecting fails with `CA cert does not include key usage extension`,
53
+ > set `lenient_tls: true` in your config — see [Troubleshooting](https://github.com/BioWilko/squarepeg/wiki/Troubleshooting#runner-errors-exit-code-125).
54
+
52
55
  See also [`examples/config.yaml`](examples/config.yaml) for a fully worked
53
56
  config file.
54
57
 
@@ -26,10 +26,11 @@ class FakeSession:
26
26
  .core/.batch are plain MagicMocks returning empty lists by default, for 'clean' tests
27
27
  that need a listable (but otherwise inert) session."""
28
28
 
29
- def __init__(self, namespace=None, context=None, quiet=False):
29
+ def __init__(self, namespace=None, context=None, quiet=False, lenient_tls=False):
30
30
  self.namespace = namespace or "default"
31
31
  self.context = context
32
32
  self.quiet = quiet
33
+ self.lenient_tls = lenient_tls
33
34
  self.context_name = context or "fake-context"
34
35
  self.core = MagicMock()
35
36
  self.batch = MagicMock()
@@ -318,11 +319,12 @@ def test_dry_run_server_constructs_session_with_context_and_quiet(monkeypatch):
318
319
  constructed = {}
319
320
 
320
321
  class RecordingFakeSession(FakeSession):
321
- def __init__(self, namespace=None, context=None, quiet=False):
322
+ def __init__(self, namespace=None, context=None, quiet=False, lenient_tls=False):
322
323
  constructed["namespace"] = namespace
323
324
  constructed["context"] = context
324
325
  constructed["quiet"] = quiet
325
- super().__init__(namespace=namespace, context=context, quiet=quiet)
326
+ constructed["lenient_tls"] = lenient_tls
327
+ super().__init__(namespace=namespace, context=context, quiet=quiet, lenient_tls=lenient_tls)
326
328
 
327
329
  monkeypatch.setattr(cli_module, "Session", RecordingFakeSession)
328
330
  monkeypatch.setattr(cli_module, "server_dry_run", lambda session, spec, manifest: manifest)
@@ -333,6 +335,27 @@ def test_dry_run_server_constructs_session_with_context_and_quiet(monkeypatch):
333
335
  assert result.exit_code == 0, result.output
334
336
  assert constructed["context"] == "my-context"
335
337
  assert constructed["quiet"] is True
338
+ assert constructed["lenient_tls"] is False
339
+
340
+
341
+ def test_dry_run_server_lenient_tls_from_config(monkeypatch, tmp_path):
342
+ constructed = {}
343
+
344
+ class RecordingFakeSession(FakeSession):
345
+ def __init__(self, namespace=None, context=None, quiet=False, lenient_tls=False):
346
+ constructed["lenient_tls"] = lenient_tls
347
+ super().__init__(namespace=namespace, context=context, quiet=quiet, lenient_tls=lenient_tls)
348
+
349
+ monkeypatch.setattr(cli_module, "Session", RecordingFakeSession)
350
+ monkeypatch.setattr(cli_module, "server_dry_run", lambda session, spec, manifest: manifest)
351
+ config_file = tmp_path / "config.yaml"
352
+ config_file.write_text("lenient_tls: true\n")
353
+
354
+ result = CliRunner().invoke(
355
+ cli, ["run", "--no-default-config", "--config", str(config_file), "--dry-run-server", "alpine"]
356
+ )
357
+ assert result.exit_code == 0, result.output
358
+ assert constructed["lenient_tls"] is True
336
359
 
337
360
 
338
361
  def test_dry_run_server_apiserver_rejection_exits_125(monkeypatch):
@@ -354,7 +377,7 @@ def test_dry_run_server_apiserver_rejection_exits_125(monkeypatch):
354
377
 
355
378
 
356
379
  def test_dry_run_server_no_cluster_connection_exits_125(monkeypatch):
357
- def raising_session(namespace=None, context=None, quiet=False):
380
+ def raising_session(namespace=None, context=None, quiet=False, lenient_tls=False):
358
381
  raise RunnerError("could not load a kubeconfig (...) and doesn't look like it's running in-cluster (...)")
359
382
 
360
383
  monkeypatch.setattr(cli_module, "Session", raising_session)
@@ -554,8 +577,8 @@ def _mock_clean_session(monkeypatch, pods=(), jobs=()):
554
577
  monkeypatch.setattr("squarepeg.k8s.orphans.current_created_by", lambda: CLEAN_CREATED_BY)
555
578
 
556
579
  class RecordingFakeSession(FakeSession):
557
- def __init__(self, namespace=None, context=None, quiet=False):
558
- super().__init__(namespace=namespace, context=context, quiet=quiet)
580
+ def __init__(self, namespace=None, context=None, quiet=False, lenient_tls=False):
581
+ super().__init__(namespace=namespace, context=context, quiet=quiet, lenient_tls=lenient_tls)
559
582
  self.core.list_namespaced_pod.return_value = SimpleNamespace(items=list(pods))
560
583
  self.batch.list_namespaced_job.return_value = SimpleNamespace(items=list(jobs))
561
584
 
@@ -597,8 +620,8 @@ def test_clean_partial_failure_exit_code(monkeypatch):
597
620
  monkeypatch.setattr("squarepeg.k8s.orphans.current_created_by", lambda: CLEAN_CREATED_BY)
598
621
 
599
622
  class FailingFakeSession(FakeSession):
600
- def __init__(self, namespace=None, context=None, quiet=False):
601
- super().__init__(namespace=namespace, context=context, quiet=quiet)
623
+ def __init__(self, namespace=None, context=None, quiet=False, lenient_tls=False):
624
+ super().__init__(namespace=namespace, context=context, quiet=quiet, lenient_tls=lenient_tls)
602
625
  self.core.list_namespaced_pod.return_value = SimpleNamespace(items=[_clean_pod("p1"), _clean_pod("p2")])
603
626
  self.core.delete_namespaced_pod.side_effect = [ApiException(status=500, reason="err"), None]
604
627
 
@@ -613,8 +636,8 @@ def test_clean_listing_failure_exits_125(monkeypatch):
613
636
  monkeypatch.setattr("squarepeg.k8s.orphans.current_created_by", lambda: CLEAN_CREATED_BY)
614
637
 
615
638
  class RaisingFakeSession(FakeSession):
616
- def __init__(self, namespace=None, context=None, quiet=False):
617
- super().__init__(namespace=namespace, context=context, quiet=quiet)
639
+ def __init__(self, namespace=None, context=None, quiet=False, lenient_tls=False):
640
+ super().__init__(namespace=namespace, context=context, quiet=quiet, lenient_tls=lenient_tls)
618
641
  self.core.list_namespaced_pod.side_effect = ApiException(status=403, reason="Forbidden")
619
642
 
620
643
  monkeypatch.setattr(cli_module, "Session", RaisingFakeSession)
@@ -628,10 +651,10 @@ def test_clean_namespace_and_context_forwarded_to_session(monkeypatch):
628
651
  monkeypatch.setattr("squarepeg.k8s.orphans.current_created_by", lambda: CLEAN_CREATED_BY)
629
652
 
630
653
  class RecordingFakeSession(FakeSession):
631
- def __init__(self, namespace=None, context=None, quiet=False):
654
+ def __init__(self, namespace=None, context=None, quiet=False, lenient_tls=False):
632
655
  constructed["namespace"] = namespace
633
656
  constructed["context"] = context
634
- super().__init__(namespace=namespace, context=context, quiet=quiet)
657
+ super().__init__(namespace=namespace, context=context, quiet=quiet, lenient_tls=lenient_tls)
635
658
 
636
659
  monkeypatch.setattr(cli_module, "Session", RecordingFakeSession)
637
660
  result = _separated_runner().invoke(
@@ -1,3 +1,4 @@
1
+ import ssl
1
2
  from unittest.mock import MagicMock
2
3
 
3
4
  import pytest
@@ -119,3 +120,26 @@ def test_explicit_context_failure_does_not_fall_back_to_incluster(monkeypatch):
119
120
  session_module.Session(context="myctx", quiet=True)
120
121
 
121
122
  incluster.assert_not_called()
123
+
124
+
125
+ def _pool_kw(api):
126
+ return api.api_client.rest_client.pool_manager.connection_pool_kw
127
+
128
+
129
+ def test_default_session_injects_no_ssl_context(monkeypatch):
130
+ monkeypatch.setattr(session_module.kube_config, "load_kube_config", MagicMock())
131
+ monkeypatch.setattr(session_module.kube_config, "list_kube_config_contexts", MagicMock(return_value=_ctx()))
132
+ s = session_module.Session(quiet=True)
133
+ assert "ssl_context" not in _pool_kw(s.core)
134
+ assert "ssl_context" not in _pool_kw(s.batch)
135
+
136
+
137
+ def test_lenient_tls_clears_only_x509_strict(monkeypatch):
138
+ monkeypatch.setattr(session_module.kube_config, "load_kube_config", MagicMock())
139
+ monkeypatch.setattr(session_module.kube_config, "list_kube_config_contexts", MagicMock(return_value=_ctx()))
140
+ s = session_module.Session(quiet=True, lenient_tls=True)
141
+ assert s.core.api_client is s.batch.api_client
142
+ ctx = _pool_kw(s.core)["ssl_context"]
143
+ assert not ctx.verify_flags & ssl.VERIFY_X509_STRICT
144
+ assert ctx.verify_mode == ssl.CERT_REQUIRED
145
+ assert ctx.check_hostname is True
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes