From e3b373fc1f31821ab4cd9c288c0263c3bb1b1354 Mon Sep 17 00:00:00 2001 From: Raj Bhargav <72274012+p172913@users.noreply.github.com> Date: Tue, 29 Apr 2025 01:57:06 +0530 Subject: [PATCH] Added proxy variable to read values from environment What type of PR is this? /kind bug What this PR does / why we need it: This PRs will read environment variables assigned for proxy and no_proxy. Update ws_client_test.py Update configuration.py What type of PR is this? /kind bug What this PR does / why we need it: This PRs will read environment variables assigned for proxy and no_proxy. Update configuration.py Add debug logging doc and example add .readthedocs.yaml config file Added Added insert_proxy_config.sh to edit configuration.py in client Revert "Added insert_proxy_config.sh to edit configuration.py in client" This reverts commit b295c2ddcbb838196823c4d7a55a67fd1d1dc290. To avoid condition self.no_proxy is already present --- .readthedocs.yaml | 53 +++++++++ devel/debug_logging.md | 34 ++++++ examples/enable_debug_logging.py | 61 ++++++++++ kubernetes/base/stream/ws_client_test.py | 138 +++++++++++++++++++---- kubernetes/client/configuration.py | 7 ++ scripts/insert_proxy_config.sh | 77 +++++++++++++ scripts/release.sh | 3 +- 7 files changed, 352 insertions(+), 21 deletions(-) create mode 100644 .readthedocs.yaml create mode 100644 devel/debug_logging.md create mode 100644 examples/enable_debug_logging.py create mode 100644 scripts/insert_proxy_config.sh diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..d220507cec --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,53 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: doc/source/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: doc/requirements-docs.txt + - requirements: test-requirements.txt + + +# git clone --depth 1 https://github.com/kubernetes-client/python . +# git fetch origin --force --prune --prune-tags --depth 50 refs/heads/master:refs/remotes/origin/master +# git checkout --force origin/master +# git clean -d -f -f +# python3.7 -mvirtualenv $READTHEDOCS_VIRTUALENV_PATH +# python -m pip install --upgrade --no-cache-dir pip setuptools +# python -m pip install --upgrade --no-cache-dir pillow mock==1.0.1 alabaster>=0.7,<0.8,!=0.7.5 commonmark==0.9.1 recommonmark==0.5.0 sphinx<2 sphinx-rtd-theme<0.5 readthedocs-sphinx-ext<2.3 jinja2<3.1.0 + +# cat doc/source/conf.py +# python -m sphinx -T -E -b html -d _build/doctrees -D language=en . $READTHEDOCS_OUTPUT/html +# python -m sphinx -T -E -b readthedocssinglehtmllocalmedia -d _build/doctrees -D language=en . $READTHEDOCS_OUTPUT/htmlzip +# python -m sphinx -T -E -b latex -d _build/doctrees -D language=en . $READTHEDOCS_OUTPUT/pdf +# cat latexmkrc +# latexmk -r latexmkrc -pdf -f -dvi- -ps- -jobname=kubernetes -interaction=nonstopmode +# python -m sphinx -T -E -b epub -d _build/doctrees -D language=en . $READTHEDOCS_OUTPUT/epub \ No newline at end of file diff --git a/devel/debug_logging.md b/devel/debug_logging.md new file mode 100644 index 0000000000..966e3d38e4 --- /dev/null +++ b/devel/debug_logging.md @@ -0,0 +1,34 @@ +# Enabling Debug Logging in Kubernetes Python Client + +This document describes how to enable debug logging, view logged information, and provides examples for creating, patching, and deleting Kubernetes resources. + +## 1. Why Enable Debug Logging? + +Debug logging is useful for troubleshooting as it shows details like HTTP request and response headers and bodies. These details can help identify issues during interactions with the Kubernetes API server. + +--- + +## 2. Enabling Debug Logging + +To enable debug logging in the Kubernetes Python client, follow these steps: + +1. **Modify the Configuration Object:** + Enable debug mode by setting the `debug` attribute of the `client.Configuration` object to `True`. + +2. **Example Code to Enable Debug Logging:** + Below is an example showing how to enable debug logging: + ```python + from kubernetes import client, config + + # Load kube config + config.load_kube_config() + + # Enable debug logging + c = client.Configuration() + c.debug = True + + # Pass the updated configuration to the API client + api_client = client.ApiClient(configuration=c) + + # Use the API client with debug logging enabled + apps_v1 = client.AppsV1Api(api_client=api_client) diff --git a/examples/enable_debug_logging.py b/examples/enable_debug_logging.py new file mode 100644 index 0000000000..573948f3c1 --- /dev/null +++ b/examples/enable_debug_logging.py @@ -0,0 +1,61 @@ +# Copyright 2025 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# This example demonstrates how to enable debug logging in the Kubernetes +# Python client and how it can be used for troubleshooting requests/responses. + +from kubernetes import client, config + + +def main(): + # Load kubeconfig from default location + config.load_kube_config() + + # Enable debug logging + configuration = client.Configuration() + configuration.debug = True + api_client = client.ApiClient(configuration=configuration) + + # Use AppsV1Api with debug logging enabled + apps_v1 = client.AppsV1Api(api_client=api_client) + + # Example: Create a dummy deployment (adjust namespace as needed) + deployment = client.V1Deployment( + api_version="apps/v1", + kind="Deployment", + metadata=client.V1ObjectMeta(name="debug-example"), + spec=client.V1DeploymentSpec( + replicas=1, + selector={"matchLabels": {"app": "debug"}}, + template=client.V1PodTemplateSpec( + metadata=client.V1ObjectMeta(labels={"app": "debug"}), + spec=client.V1PodSpec( + containers=[ + client.V1Container( + name="busybox", + image="busybox", + command=["sh", "-c", "echo Hello, Kubernetes! && sleep 3600"] + ) + ] + ), + ), + ), + ) + + # Create the deployment + try: + print("[INFO] Creating deployment...") + apps_v1.create_namespaced_deployment( + namespace="default", body=deployment + ) + except client.exceptions.ApiException as e: + print("[ERROR] Exception occurred:", e) + + +if __name__ == "__main__": + main() diff --git a/kubernetes/base/stream/ws_client_test.py b/kubernetes/base/stream/ws_client_test.py index a7a11f5c91..575ec1cd44 100644 --- a/kubernetes/base/stream/ws_client_test.py +++ b/kubernetes/base/stream/ws_client_test.py @@ -17,19 +17,70 @@ from .ws_client import get_websocket_url from .ws_client import websocket_proxycare from kubernetes.client.configuration import Configuration +import os +import socket +import threading +import pytest +from kubernetes import stream, client, config try: import urllib3 urllib3.disable_warnings() except ImportError: pass +@pytest.fixture(autouse=True) +def dummy_kubeconfig(tmp_path, monkeypatch): + # Creating a kubeconfig + content = """ + apiVersion: v1 + kind: Config + clusters: + - name: default + cluster: + server: http://127.0.0.1:8888 + contexts: + - name: default + context: + cluster: default + user: default + users: + - name: default + user: {} + current-context: default + """ + cfg_file = tmp_path / "kubeconfig" + cfg_file.write_text(content) + monkeypatch.setenv("KUBECONFIG", str(cfg_file)) -def dictval(dict, key, default=None): - try: - val = dict[key] - except KeyError: - val = default - return val + +def dictval(dict_obj, key, default=None): + + return dict_obj.get(key, default) + +class DummyProxy(threading.Thread): + """ + A minimal HTTP proxy that flags any CONNECT request and returns 200 OK. + Listens on 127.0.0.1:8888 by default. + """ + def __init__(self, host='127.0.0.1', port=8888): + super().__init__(daemon=True) + self.host = host + self.port = port + self.received_connect = False + self._server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._server_sock.bind((self.host, self.port)) + self._server_sock.listen(1) + + def run(self): + conn, _ = self._server_sock.accept() + try: + data = conn.recv(1024).decode('utf-8', errors='ignore') + if data.startswith('CONNECT '): + self.received_connect = True + conn.sendall(b"HTTP/1.1 200 Connection established\r\n\r\n") + finally: + conn.close() class WSClientTest(unittest.TestCase): @@ -56,21 +107,68 @@ def test_websocket_proxycare(self): ( 'http://proxy.example.com:8080/', 'user:pass', '.example.com', 'proxy.example.com', 8080, ('user','pass'), ['.example.com']), ( 'http://proxy.example.com:8080/', 'user:pass', 'localhost,.local,.example.com', 'proxy.example.com', 8080, ('user','pass'), ['localhost','.local','.example.com']), ]: - # setup input - config = Configuration() - if proxy is not None: - setattr(config, 'proxy', proxy) - if idpass is not None: - setattr(config, 'proxy_headers', urllib3.util.make_headers(proxy_basic_auth=idpass)) + # input setup + cfg = Configuration() + if proxy: + cfg.proxy = proxy + if idpass: + cfg.proxy_headers = urllib3.util.make_headers(proxy_basic_auth=idpass) if no_proxy is not None: - setattr(config, 'no_proxy', no_proxy) - # setup done - # test starts - connect_opt = websocket_proxycare( {}, config, None, None) - self.assertEqual( dictval(connect_opt,'http_proxy_host'), expect_host) - self.assertEqual( dictval(connect_opt,'http_proxy_port'), expect_port) - self.assertEqual( dictval(connect_opt,'http_proxy_auth'), expect_auth) - self.assertEqual( dictval(connect_opt,'http_no_proxy'), expect_noproxy) + cfg.no_proxy = no_proxy + + + connect_opts = websocket_proxycare({}, cfg, None, None) + assert dictval(connect_opts, 'http_proxy_host') == expect_host + assert dictval(connect_opts, 'http_proxy_port') == expect_port + assert dictval(connect_opts, 'http_proxy_auth') == expect_auth + assert dictval(connect_opts, 'http_no_proxy') == expect_noproxy + +@pytest.fixture(scope="module") +def dummy_proxy(): + #Dummy Proxy + proxy = DummyProxy(port=8888) + proxy.start() + yield proxy + +@pytest.fixture(autouse=True) +def clear_proxy_env(monkeypatch): + for var in ("HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "NO_PROXY", "no_proxy"): + monkeypatch.delenv(var, raising=False) + +def apply_proxy_to_conf(): + #apply HTTPS_PROXY env var and set it as global. + cfg = client.Configuration.get_default_copy() + cfg.proxy = os.getenv("HTTPS_PROXY") + cfg.no_proxy = os.getenv("NO_PROXY", "") + client.Configuration.set_default(cfg) + +def test_rest_call_ignores_env(dummy_proxy, monkeypatch): + # HTTPS_PROXY to dummy proxy + monkeypatch.setenv("HTTPS_PROXY", "http://127.0.0.1:8888") + # Avoid real HTTP request + monkeypatch.setattr(client.CoreV1Api, "list_namespace", lambda self, *_args, **_kwargs: None) + # Load config using kubeconfig + config.load_kube_config(config_file=os.environ["KUBECONFIG"]) + apply_proxy_to_conf() + # HTTPS_PROXY to dummy proxy + monkeypatch.setenv("HTTPS_PROXY", "http://127.0.0.1:8888") + config.load_kube_config(config_file=os.environ["KUBECONFIG"]) + apply_proxy_to_conf() + v1 = client.CoreV1Api() + v1.list_namespace(_preload_content=False) + assert not dummy_proxy.received_connect, "REST path should ignore HTTPS_PROXY" + +def test_websocket_call_honors_env(dummy_proxy, monkeypatch): + # set HTTPS_PROXY again + monkeypatch.setenv("HTTPS_PROXY", "http://127.0.0.1:8888") + # Load kubeconfig + config.load_kube_config(config_file=os.environ["KUBECONFIG"]) + apply_proxy_to_conf() + opts = websocket_proxycare({}, client.Configuration.get_default_copy(), None, None) + assert opts.get('http_proxy_host') == '127.0.0.1' + assert opts.get('http_proxy_port') == 8888 + # Optionally verify no_proxy parsing + assert opts.get('http_no_proxy') is None if __name__ == '__main__': unittest.main() diff --git a/kubernetes/client/configuration.py b/kubernetes/client/configuration.py index e1c0ff2dc5..f88fad9371 100644 --- a/kubernetes/client/configuration.py +++ b/kubernetes/client/configuration.py @@ -17,6 +17,7 @@ import multiprocessing import sys import urllib3 +import os import six from six.moves import http_client as httplib @@ -158,9 +159,15 @@ def __init__(self, host="http://localhost", """ self.proxy = None + if(os.getenv("HTTPS_PROXY")):self.proxy=os.getenv("HTTPS_PROXY") + if(os.getenv("https_proxy")):self.proxy=os.getenv("https_proxy") + if(os.getenv("HTTP_PROXY")):self.proxy=os.getenv("HTTP_PROXY") + if(os.getenv("http_proxy")):self.proxy=os.getenv("http_proxy") """Proxy URL """ self.no_proxy = None + if(os.getenv("NO_PROXY")):self.no_proxy=os.getenv("NO_PROXY") + if(os.getenv("no_proxy")):self.no_proxy=os.getenv("no_proxy") """bypass proxy for host in the no_proxy list. """ self.proxy_headers = None diff --git a/scripts/insert_proxy_config.sh b/scripts/insert_proxy_config.sh new file mode 100644 index 0000000000..a4f0b70956 --- /dev/null +++ b/scripts/insert_proxy_config.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# insert_proxy_config.sh - run this after openapi-generator release.sh +CONFIG_PATH="../python_kubernetes/kubernetes/client" + +# Compute the full file path +CONFIG_FILE="$CONFIG_PATH/configuration.py" + +# --- Normalize Windows-style backslashes to Unix forward slashes --- +CONFIG_FILE="$(echo "$CONFIG_FILE" | sed 's|\\|/|g')" + +# --- Ensure the target file exists and is writable --- +if [ ! -f "$CONFIG_FILE" ] || [ ! -w "$CONFIG_FILE" ]; then + echo "Error: $CONFIG_FILE does not exist or is not writable." >&2 + exit 1 +fi + +# --- Step 1: Ensure 'import os' follows existing imports (idempotent) --- +if ! grep -qE '^import os$' "$CONFIG_FILE"; then + LAST_IMPORT=$(grep -nE '^(import |from )' "$CONFIG_FILE" | tail -n1 | cut -d: -f1) + if [ -n "$LAST_IMPORT" ]; then + sed -i "$((LAST_IMPORT+1))i import os" "$CONFIG_FILE" + else + if head -n1 "$CONFIG_FILE" | grep -q '^#!'; then + sed -i '2i import os' "$CONFIG_FILE" + else + sed -i '1i import os' "$CONFIG_FILE" + fi + fi + echo "Inserted 'import os' after existing imports in $CONFIG_FILE." +else + echo "'import os' already present; no changes made." +fi + +# --- Step 2: Insert proxy & no_proxy environment code --- +if ! grep -q 'os.getenv("HTTPS_PROXY"' "$CONFIG_FILE"; then + PROXY_LINE=$(grep -n "self.proxy = None" "$CONFIG_FILE" | cut -d: -f1) + NO_PROXY_LINE=$(grep -n "^[[:space:]]*self\.no_proxy[[:space:]]*=[[:space:]]*None" "$CONFIG_FILE" | cut -d: -f1) + + if [ -n "$PROXY_LINE" ]; then + INDENT=$(sed -n "${PROXY_LINE}s/^\( *\).*/\1/p" "$CONFIG_FILE") + + BLOCK="" + + if [ -z "$NO_PROXY_LINE" ]; then + # self.no_proxy = None is not present → insert full block after self.proxy = None + BLOCK+="${INDENT}# Load proxy from environment variables (if set)\n" + BLOCK+="${INDENT}if os.getenv(\"HTTPS_PROXY\"): self.proxy = os.getenv(\"HTTPS_PROXY\")\n" + BLOCK+="${INDENT}if os.getenv(\"https_proxy\"): self.proxy = os.getenv(\"https_proxy\")\n" + BLOCK+="${INDENT}if os.getenv(\"HTTP_PROXY\"): self.proxy = os.getenv(\"HTTP_PROXY\")\n" + BLOCK+="${INDENT}if os.getenv(\"http_proxy\"): self.proxy = os.getenv(\"http_proxy\")\n" + BLOCK+="${INDENT}self.no_proxy = None\n" + BLOCK+="${INDENT}# Load no_proxy from environment variables (if set)\n" + BLOCK+="${INDENT}if os.getenv(\"NO_PROXY\"): self.no_proxy = os.getenv(\"NO_PROXY\")\n" + BLOCK+="${INDENT}if os.getenv(\"no_proxy\"): self.no_proxy = os.getenv(\"no_proxy\")" + + sed -i "${PROXY_LINE}a $BLOCK" "$CONFIG_FILE" + echo "Inserted full proxy + no_proxy block after 'self.proxy = None'." + else + # self.no_proxy = None exists → insert only env logic after that + BLOCK+="${INDENT}# Load proxy from environment variables (if set)\n" + BLOCK+="${INDENT}if os.getenv(\"HTTPS_PROXY\"): self.proxy = os.getenv(\"HTTPS_PROXY\")\n" + BLOCK+="${INDENT}if os.getenv(\"https_proxy\"): self.proxy = os.getenv(\"https_proxy\")\n" + BLOCK+="${INDENT}if os.getenv(\"HTTP_PROXY\"): self.proxy = os.getenv(\"HTTP_PROXY\")\n" + BLOCK+="${INDENT}if os.getenv(\"http_proxy\"): self.proxy = os.getenv(\"http_proxy\")\n" + BLOCK+="${INDENT}# Load no_proxy from environment variables (if set)\n" + BLOCK+="${INDENT}if os.getenv(\"NO_PROXY\"): self.no_proxy = os.getenv(\"NO_PROXY\")\n" + BLOCK+="${INDENT}if os.getenv(\"no_proxy\"): self.no_proxy = os.getenv(\"no_proxy\")" + + sed -i "${NO_PROXY_LINE}a $BLOCK" "$CONFIG_FILE" + echo "Inserted environment block after 'self.no_proxy = None'." + fi + else + echo "Warning: 'self.proxy = None' not found in $CONFIG_FILE. No proxy code inserted." + fi +else + echo "Proxy environment code already present; no changes made." +fi \ No newline at end of file diff --git a/scripts/release.sh b/scripts/release.sh index dce27298d2..b037204c02 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -207,7 +207,8 @@ git diff-index --quiet --cached HEAD || git commit -am "update changelog" # Re-generate the client scripts/update-client.sh - +#edit comfiguration.py files +scripts/insert_proxy_config.sh # Apply hotfixes rm -r kubernetes/test/ git add .