Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/kubernator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
os:
- ubuntu-latest
python-version:
- '3.14-dev'
- '3.13'
- '3.12'
- '3.11'
Expand All @@ -30,7 +31,7 @@ jobs:
# - os: macos-12
# python-version: '3.11'
env:
DEPLOY_PYTHONS: "3.12"
DEPLOY_PYTHONS: "3.13"
DEPLOY_OSES: "Linux"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TWINE_USERNAME: __token__
Expand Down
6 changes: 3 additions & 3 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"Source Code": "https://github.com/karellen/kubernator/",
"Documentation": "https://github.com/karellen/kubernator/"
}
license = "Apache License, Version 2.0"
license = "Apache-2.0"

requires_python = ">=3.9"

Expand All @@ -56,7 +56,7 @@ def set_properties(project):
project.depends_on("openapi-spec-validator", "~=0.3")
project.depends_on("json-log-formatter", "~=0.3")
project.depends_on("platformdirs", "~=4.2")
project.depends_on("requests", "<=2.31.0")
project.depends_on("requests", ">=2.31.0")
project.depends_on("jsonpatch", "~=1.33")
project.depends_on("jsonpath-ng", "~=1.7.0")
project.depends_on("jinja2", "~=3.1")
Expand All @@ -83,12 +83,12 @@ def set_properties(project):
"kOps", "terraform", "tf", "AWS"])

project.set_property("distutils_classifiers", [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX",
"Operating System :: POSIX :: Linux",
Expand Down
8 changes: 4 additions & 4 deletions src/integrationtest/python/full_smoke_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ class FullSmokeTest(IntegrationTestSupport):
def test_full_smoke(self):
test_dir = Path(__file__).parent / "full_smoke"

for k8s_version, istio_version in ((self.K8S_TEST_VERSIONS[-4], "1.23.6"),
(self.K8S_TEST_VERSIONS[-3], "1.24.6"),
(self.K8S_TEST_VERSIONS[-2], "1.25.3"),
(self.K8S_TEST_VERSIONS[-1], "1.26.0")):
for k8s_version, istio_version in ((self.K8S_TEST_VERSIONS[-4], "1.24.6"),
(self.K8S_TEST_VERSIONS[-3], "1.25.5"),
(self.K8S_TEST_VERSIONS[-2], "1.26.4"),
(self.K8S_TEST_VERSIONS[-1], "1.27.1"),):
with self.subTest(k8s_version=k8s_version, istio_version=istio_version):
os.environ["K8S_VERSION"] = k8s_version
os.environ["ISTIO_VERSION"] = istio_version
Expand Down
2 changes: 1 addition & 1 deletion src/integrationtest/python/issue_23_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_issue_23(self):
test_dir = Path(__file__).parent / "issue_23"

for k8s_version, istio_version in ((self.K8S_TEST_VERSIONS[0], "1.10.6"),
(self.K8S_TEST_VERSIONS[-1], "1.23.1")):
(self.K8S_TEST_VERSIONS[-1], "1.27.1")):
with self.subTest(k8s_version=k8s_version, istio_version=istio_version):
os.environ["K8S_VERSION"] = k8s_version
os.environ["ISTIO_VERSION"] = istio_version
Expand Down
4 changes: 2 additions & 2 deletions src/integrationtest/python/issue_52_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class Issue52Test(IntegrationTestSupport):
def test_issue_52(self):
test_dir = Path(__file__).parent / "issue_52"

for k8s_version, istio_version in ((self.K8S_TEST_VERSIONS[-3], "1.20.4"),
(self.K8S_TEST_VERSIONS[-1], "1.22.0")):
for k8s_version, istio_version in ((self.K8S_TEST_VERSIONS[-6], "1.20.4"),
(self.K8S_TEST_VERSIONS[-1], "1.27.1")):
with self.subTest(k8s_version=k8s_version, istio_version=istio_version):
os.environ["K8S_VERSION"] = k8s_version
os.environ["ISTIO_VERSION"] = istio_version
Expand Down
4 changes: 2 additions & 2 deletions src/integrationtest/python/smoke_pre_cache_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ def setUp(self):
self.run_module_test("kubernator", "--clear-k8s-cache")

def test_precache(self):
for k8s_version in range(19, 29):
for k8s_version in range(19, 35):
for disable_patches in (True, False, True):
with self.subTest(k8s_version=k8s_version, disable_patches=disable_patches):
args = ["kubernator", "--pre-cache-k8s-client", str(k8s_version)]
args = ["kubernator", "-v", "TRACE", "--pre-cache-k8s-client", str(k8s_version)]
if disable_patches:
args.append("--pre-cache-k8s-client-no-patch")
self.run_module_test(*args)
Expand Down
4 changes: 2 additions & 2 deletions src/integrationtest/python/test_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ class IntegrationTestSupport(unittest.TestCase):
K8S_TEST_VERSIONS = ["1.20.15", "1.21.14", "1.22.17",
"1.23.17", "1.24.17", "1.25.16",
"1.26.15", "1.27.16", "1.28.15",
"1.29.15", "1.30.13", "1.31.9",
"1.32.5", "1.33.1"]
"1.29.15", "1.30.14", "1.31.12",
"1.32.8", "1.33.4", "1.34.0"]

def load_json_logs(self, log_file):
decoder = json.JSONDecoder()
Expand Down
42 changes: 36 additions & 6 deletions src/main/python/kubernator/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,8 @@ def handle_summary(self):
pass


def install_python_k8s_client(run, package_major, logger, logger_stdout, logger_stderr, disable_patching):
def install_python_k8s_client(run, package_major, logger, logger_stdout, logger_stderr, disable_patching,
fallback=False):
cache_dir = get_cache_dir("python")
package_major_dir = cache_dir / str(package_major)
package_major_dir_str = str(package_major_dir)
Expand All @@ -780,14 +781,43 @@ def install_python_k8s_client(run, package_major, logger, logger_stdout, logger_
str(package_major), package_major_dir)
rmtree(package_major_dir)

if not package_major_dir.exists():
if not package_major_dir.exists() or not len(os.listdir(package_major_dir)):
package_major_dir.mkdir(parents=True, exist_ok=True)
run([sys.executable, "-m", "pip", "install", "--no-deps", "--no-input",
"--root-user-action=ignore", "--break-system-packages", "--disable-pip-version-check",
"--target", package_major_dir_str, f"kubernetes>={package_major!s}dev0,<{int(package_major) + 1!s}"],
logger_stdout, logger_stderr).wait()
try:
run([sys.executable, "-m", "pip", "install", "--no-deps", "--no-input",
"--root-user-action=ignore", "--break-system-packages", "--disable-pip-version-check",
"--target", package_major_dir_str, f"kubernetes>={package_major!s}dev0,<{int(package_major) + 1!s}"],
logger_stdout, logger_stderr)
except CalledProcessError as e:
if not fallback and "No matching distribution found for" in e.stderr:
logger.warning("Kubernetes Client %s (%s) failed to install because the version wasn't found. "
"Falling back to a client of the previous version - %s",
str(package_major), package_major_dir, int(package_major) - 1)
return install_python_k8s_client(run,
int(package_major) - 1,
logger,
logger_stdout,
logger_stderr,
disable_patching,
fallback=True)
else:
raise

if not patch_indicator.exists() and not disable_patching:
if not fallback and not len(os.listdir(package_major_dir)):
# Directory is empty
logger.warning("Kubernetes Client %s (%s) directory is empty - the client was not installed. "
"Falling back to a client of the previous version - %s",
str(package_major), package_major_dir, int(package_major) - 1)

return install_python_k8s_client(run,
int(package_major) - 1,
logger,
logger_stdout,
logger_stderr,
disable_patching,
fallback=True)

for patch_text, target_file, skip_if_found, min_version, max_version, name in (
URLLIB_HEADERS_PATCH, CUSTOM_OBJECT_PATCH_23, CUSTOM_OBJECT_PATCH_25):
patch_target = package_major_dir / target_file
Expand Down
10 changes: 7 additions & 3 deletions src/main/python/kubernator/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from kubernator.api import (KubernatorPlugin, Globs, scan_dir, PropertyDict, config_as_dict, config_parent,
download_remote_file, load_remote_file, Repository, StripNL, jp, get_app_cache_dir,
get_cache_dir, install_python_k8s_client)
from kubernator.proc import run, run_capturing_out
from kubernator.proc import run, run_capturing_out, run_pass_through_capturing

TRACE = 5

Expand Down Expand Up @@ -215,7 +215,7 @@ def run(self):
self.context = self._top_dir_context
context = self.context
self._run_handlers(KubernatorPlugin.handle_shutdown, True, context, None)
except: # noqa E722
except: # noqa E722
raise
else:
self.context = self._top_dir_context
Expand Down Expand Up @@ -348,6 +348,7 @@ def handle_init(self):
jp=jp,
run=self._run,
run_capturing_out=self._run_capturing_out,
run_passthrough_capturing=self._run_passthrough_capturing,
repository=self.repository,
StripNL=StripNL,
default_includes=Globs(["*"], True),
Expand Down Expand Up @@ -465,6 +466,9 @@ def _run(self, *args, **kwargs):
def _run_capturing_out(self, *args, **kwargs):
return run_capturing_out(*args, **kwargs)

def _run_passthrough_capturing(self, *args, **kwargs):
return run_pass_through_capturing(*args, **kwargs)

def __repr__(self):
return "Kubernator"

Expand Down Expand Up @@ -493,7 +497,7 @@ def pre_cache_k8s_clients(*versions, disable_patching=False):
for v in versions:
logger.info("Caching K8S client library ~=v%s.0%s...", v,
" (no patches)" if disable_patching else "")
install_python_k8s_client(run, v, logger, stdout_logger, stderr_logger, disable_patching)
install_python_k8s_client(run_pass_through_capturing, v, logger, stdout_logger, stderr_logger, disable_patching)


def main():
Expand Down
2 changes: 1 addition & 1 deletion src/main/python/kubernator/plugins/k8s.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def setup_client(self):

logger.info("Using Kubernetes client version =~%s.0 for server version %s",
server_minor, ".".join(k8s.server_version))
pkg_dir = install_python_k8s_client(self.context.app.run, server_minor, logger,
pkg_dir = install_python_k8s_client(self.context.app.run_passthrough_capturing, server_minor, logger,
stdout_logger, stderr_logger, k8s.disable_client_patches)

modules_to_delete = []
Expand Down
10 changes: 10 additions & 0 deletions src/main/python/kubernator/plugins/minikube.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
stdout_logger = StripNL(proc_logger.info)
stderr_logger = StripNL(proc_logger.warning)

MINIKUBE_MAX_VERSION_LEGACY = "1.36.0"


class MinikubePlugin(KubernatorPlugin):
logger = logger
Expand Down Expand Up @@ -99,9 +101,17 @@ def register(self, minikube_version=None, profile="default", k8s_version=None,
logger.critical(msg)
raise RuntimeError(msg)

k8s_version_tuple = tuple(map(int, k8s_version.split(".")))

if not minikube_version:
minikube_version = self.get_latest_minikube_version()
logger.info("No minikube version is specified, latest is %s", minikube_version)
if k8s_version_tuple < (1, 28, 0):
logger.info("While latest minikube version is %s, "
"the requested K8S version %s requires %s or earlier - choosing %s",
minikube_version, k8s_version, MINIKUBE_MAX_VERSION_LEGACY,
MINIKUBE_MAX_VERSION_LEGACY)
minikube_version = MINIKUBE_MAX_VERSION_LEGACY

minikube_dl_file, _ = context.app.download_remote_file(logger,
f"https://github.com/kubernetes/minikube/releases"
Expand Down
26 changes: 24 additions & 2 deletions src/main/python/kubernator/proc.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def stdin(self):
raise RuntimeError("not available")
return self._proc.stdin

def wait(self, fail=True, timeout=None, _out_func=None):
def wait(self, fail=True, timeout=None, _out_func=None, _stderr_func=None):
with Timeout(timeout, TimeoutExpired):
retcode = self._proc.wait()
if self._stdin_writer:
Expand All @@ -119,9 +119,12 @@ def wait(self, fail=True, timeout=None, _out_func=None):
self._stderr_reader.join()
if fail and retcode:
output = None
stderr = None
if _out_func:
output = _out_func()
raise CalledProcessError(retcode, self._safe_args, output=output)
if _stderr_func:
stderr = _stderr_func()
raise CalledProcessError(retcode, self._safe_args, output=output, stderr=stderr)
return retcode

def terminate(self):
Expand All @@ -134,6 +137,25 @@ def kill(self):
run = ProcessRunner


def run_pass_through_capturing(args, stdout_logger, stderr_logger, stdin=DEVNULL, *, safe_args=None,
universal_newlines=True, **kwargs):
out = StringIO(trimmed=False) if universal_newlines else BytesIO()
err = StringIO(trimmed=False) if universal_newlines else BytesIO()

def write_out(data):
out.write(data)
stdout_logger(data)

def write_err(data):
err.write(data)
stderr_logger(data)

proc = run(args, write_out, write_err, stdin, safe_args=safe_args, universal_newlines=universal_newlines,
**kwargs)
proc.wait(_out_func=lambda: out.getvalue(), _stderr_func=lambda: err.getvalue())
return out.getvalue(), err.getvalue()


def run_capturing_out(args, stderr_logger, stdin=DEVNULL, *, safe_args=None, universal_newlines=True, **kwargs):
out = StringIO(trimmed=False) if universal_newlines else BytesIO()
proc = run(args, out.write, stderr_logger, stdin, safe_args=safe_args, universal_newlines=universal_newlines,
Expand Down