From 9fde377360db4c2f3db24a1d9b106b3bba2dd06d Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Fri, 19 Jun 2026 16:25:33 +0300 Subject: [PATCH 01/26] feat: add KeycloakClientInfoFetcher for Keycloak client information retrieval --- src/github_actions/common/__init__.py | 2 + src/github_actions/common/utils/keycloak.py | 49 +++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/github_actions/common/utils/keycloak.py diff --git a/src/github_actions/common/__init__.py b/src/github_actions/common/__init__.py index 87da7e4..52f3af7 100644 --- a/src/github_actions/common/__init__.py +++ b/src/github_actions/common/__init__.py @@ -18,6 +18,7 @@ from .utils.github_environment_variables import GitHubContext from .utils.install_rmk import RMKInstaller from .utils.install_kodjin_cli import KodjinCLIInstaller +from .utils.keycloak import KeycloakClientInfoFetcher __all__ = [ @@ -41,6 +42,7 @@ "GETTenant", "GitHubContext", "GitHubOutput", + "KeycloakClientInfoFetcher", "ProjectInitializer", "RMKConfigInitCommand", "RMKInstaller", diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py new file mode 100644 index 0000000..baa7405 --- /dev/null +++ b/src/github_actions/common/utils/keycloak.py @@ -0,0 +1,49 @@ +import requests + + +class KeycloakClientInfoFetcher: + def __init__( + self, + keycloak_url: str, + username: str, + client_id: str, + password: str, + clients_realm: str, + token_realm: str = "master", + ): + self.base_url = keycloak_url.rstrip("/") + self.username = username + self.client_id = client_id + self.password = password + self.token_realm = token_realm + self.clients_realm = clients_realm + + def get_client_info(self) -> list[dict]: + access_token = self._get_access_token() + clients_response = requests.get( + f"{self.base_url}/admin/realms/{self.clients_realm}/clients", + headers={"Authorization": f"Bearer {access_token}"}, + params={"clientId": self.client_id}, + timeout=30, + ) + clients_response.raise_for_status() + return clients_response.json() + + def _get_access_token(self) -> str: + token_response = requests.post( + f"{self.base_url}/realms/{self.token_realm}/protocol/openid-connect/token", + headers={"content-type": "application/x-www-form-urlencoded"}, + data={ + "client_id": self.client_id, + "username": self.username, + "grant_type": "password", + "password": self.password, + }, + timeout=30, + ) + token_response.raise_for_status() + + access_token = token_response.json().get("access_token") + if not access_token: + raise ValueError("access_token was not returned by Keycloak token endpoint") + return access_token From 2b8876594196d67f3b2d7443d6a48a5264e77bac Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 22 Jun 2026 17:53:24 +0300 Subject: [PATCH 02/26] feat: implement Kubectl class for managing kubectl installation and secret retrieval --- src/github_actions/common/utils/kubectl.py | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/github_actions/common/utils/kubectl.py diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py new file mode 100644 index 0000000..7f8b18f --- /dev/null +++ b/src/github_actions/common/utils/kubectl.py @@ -0,0 +1,34 @@ +import subprocess + + +class Kubectl: + def __init__(self, args): + self.kubectl_version = args.kubectl_version + self.kubectl_download_url = args.kubectl_download_url + self.install_kubectl() + + def install_kubectl(self): + print("Installing kubectl.") + try: + subprocess.run(["bash", "-s", "--", self.kubectl_version, self.kubectl_download_url], check=True, text=True, input="") + except subprocess.CalledProcessError as err: + raise Exception(f"installing kubectl:\n{err}") + + def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str: + try: + result = subprocess.run( + ["kubectl", "get", "secret", secret_name, "-n", namespace, "-o", "yaml"], check=True, text=True, capture_output=True + ) + # kubectl get secret keycloak-cluster-initial-admin --namespace keycloak --output yaml | yq '.data.password | @base64d' + + # parse yaml output and extract the password field, decode it from base64 + result = subprocess.run( + ["yq", f".data.{secret_path} | @base64d"], + input=result.stdout, + check=True, + text=True, + capture_output=True, + ) + return result.stdout + except subprocess.CalledProcessError as err: + raise Exception(f"getting secret {secret_name} in namespace {namespace}:\n{err}") From 03151bb1f09ef95ddd8a752f83946affb46fc4dc Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 22 Jun 2026 18:09:18 +0300 Subject: [PATCH 03/26] feat: refactor Kubectl class to simplify initialization and installation process --- src/github_actions/common/utils/kubectl.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index 7f8b18f..07b550d 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -2,15 +2,14 @@ class Kubectl: - def __init__(self, args): - self.kubectl_version = args.kubectl_version - self.kubectl_download_url = args.kubectl_download_url + def __init__(self, kubectl_download_url="https://dl.k8s.io/release/v1.36.2/bin/linux/amd64/kubectl"): + self.kubectl_download_url = kubectl_download_url self.install_kubectl() def install_kubectl(self): print("Installing kubectl.") try: - subprocess.run(["bash", "-s", "--", self.kubectl_version, self.kubectl_download_url], check=True, text=True, input="") + subprocess.run(["bash", "-s", "--", self.kubectl_download_url], check=True, text=True, input="") except subprocess.CalledProcessError as err: raise Exception(f"installing kubectl:\n{err}") @@ -19,7 +18,6 @@ def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str: result = subprocess.run( ["kubectl", "get", "secret", secret_name, "-n", namespace, "-o", "yaml"], check=True, text=True, capture_output=True ) - # kubectl get secret keycloak-cluster-initial-admin --namespace keycloak --output yaml | yq '.data.password | @base64d' # parse yaml output and extract the password field, decode it from base64 result = subprocess.run( From 9c33ae0c12095254e154faf63dcc66d84bdff8b3 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 22 Jun 2026 18:11:40 +0300 Subject: [PATCH 04/26] feat: add Kubectl to module exports in __init__.py --- src/github_actions/common/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/github_actions/common/__init__.py b/src/github_actions/common/__init__.py index 52f3af7..120e649 100644 --- a/src/github_actions/common/__init__.py +++ b/src/github_actions/common/__init__.py @@ -18,6 +18,7 @@ from .utils.github_environment_variables import GitHubContext from .utils.install_rmk import RMKInstaller from .utils.install_kodjin_cli import KodjinCLIInstaller +from .utils.kubectl import Kubectl from .utils.keycloak import KeycloakClientInfoFetcher @@ -49,5 +50,6 @@ "S3BucketManager", "SlackNotifier", "KodjinCLIInstaller", + "Kubectl", "GetRootDomain", ] From 645dd9ffe5932f6968eb04c31e6ca1c7e0b14bdc Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 22 Jun 2026 18:26:19 +0300 Subject: [PATCH 05/26] refactor: move install_kubectl method to a private method and update constructor --- src/github_actions/common/utils/kubectl.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index 07b550d..1dd0989 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -4,14 +4,7 @@ class Kubectl: def __init__(self, kubectl_download_url="https://dl.k8s.io/release/v1.36.2/bin/linux/amd64/kubectl"): self.kubectl_download_url = kubectl_download_url - self.install_kubectl() - - def install_kubectl(self): - print("Installing kubectl.") - try: - subprocess.run(["bash", "-s", "--", self.kubectl_download_url], check=True, text=True, input="") - except subprocess.CalledProcessError as err: - raise Exception(f"installing kubectl:\n{err}") + self._install_kubectl() def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str: try: @@ -30,3 +23,10 @@ def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str: return result.stdout except subprocess.CalledProcessError as err: raise Exception(f"getting secret {secret_name} in namespace {namespace}:\n{err}") + + def _install_kubectl(self): + print("Installing kubectl.") + try: + subprocess.run(["bash", "-s", "--", self.kubectl_download_url], check=True, text=True, input="") + except subprocess.CalledProcessError as err: + raise Exception(f"installing kubectl:\n{err}") From 6d6aa97cf814b7a5f953990fc2524bdfcaa5dbb7 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 22 Jun 2026 18:40:19 +0300 Subject: [PATCH 06/26] feat: enhance get_secret method with additional logging and fix yq command syntax --- src/github_actions/common/utils/kubectl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index 1dd0989..701c030 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -8,13 +8,15 @@ def __init__(self, kubectl_download_url="https://dl.k8s.io/release/v1.36.2/bin/l def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str: try: + print(f"Getting secret {secret_name} in namespace {namespace}.") result = subprocess.run( ["kubectl", "get", "secret", secret_name, "-n", namespace, "-o", "yaml"], check=True, text=True, capture_output=True ) + print(f"Secret {secret_name} retrieved successfully. Extracting data.") # parse yaml output and extract the password field, decode it from base64 result = subprocess.run( - ["yq", f".data.{secret_path} | @base64d"], + ["yq", f"'.data.{secret_path} | @base64d'"], input=result.stdout, check=True, text=True, From bc5c8b42ca8e4016909fc4baccb1c45887f9e235 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 22 Jun 2026 18:57:02 +0300 Subject: [PATCH 07/26] feat: refactor Kubectl class to utilize run_command for subprocess calls --- src/github_actions/common/utils/kubectl.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index 701c030..881545a 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -1,7 +1,10 @@ import subprocess -class Kubectl: +from ..utils.cmd import BaseCommand, CMDInterface + + +class Kubectl(BaseCommand, CMDInterface): def __init__(self, kubectl_download_url="https://dl.k8s.io/release/v1.36.2/bin/linux/amd64/kubectl"): self.kubectl_download_url = kubectl_download_url self._install_kubectl() @@ -9,14 +12,14 @@ def __init__(self, kubectl_download_url="https://dl.k8s.io/release/v1.36.2/bin/l def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str: try: print(f"Getting secret {secret_name} in namespace {namespace}.") - result = subprocess.run( - ["kubectl", "get", "secret", secret_name, "-n", namespace, "-o", "yaml"], check=True, text=True, capture_output=True + result = self.run_command( + f"kubectl get secret {secret_name} --namespace {namespace} --output yaml", check=True, text=True, capture_output=True ) print(f"Secret {secret_name} retrieved successfully. Extracting data.") # parse yaml output and extract the password field, decode it from base64 - result = subprocess.run( - ["yq", f"'.data.{secret_path} | @base64d'"], + result = self.run_command( + f"yq '.data.{secret_path} | @base64d'", input=result.stdout, check=True, text=True, @@ -29,6 +32,9 @@ def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str: def _install_kubectl(self): print("Installing kubectl.") try: - subprocess.run(["bash", "-s", "--", self.kubectl_download_url], check=True, text=True, input="") + self.run_command(f"bash -s -- {self.kubectl_download_url}", check=True, text=True, input="") + + version = self.run_command("kubectl version --client", check=True, text=True, capture_output=True) + print(f"kubectl installed successfully. Version: {version.stdout.strip()}") except subprocess.CalledProcessError as err: raise Exception(f"installing kubectl:\n{err}") From 304e81c3c7b3a33eef9a8ed9fae76e55cdcb0f84 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 22 Jun 2026 19:05:56 +0300 Subject: [PATCH 08/26] refactor: remove unused import and simplify exception handling in Kubectl class --- src/github_actions/common/utils/kubectl.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index 881545a..eb667a4 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -1,6 +1,3 @@ -import subprocess - - from ..utils.cmd import BaseCommand, CMDInterface @@ -9,6 +6,12 @@ def __init__(self, kubectl_download_url="https://dl.k8s.io/release/v1.36.2/bin/l self.kubectl_download_url = kubectl_download_url self._install_kubectl() + def execute(self): + self.run() + + def run(self): + print("Kubectl is ready to use.") + def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str: try: print(f"Getting secret {secret_name} in namespace {namespace}.") @@ -26,7 +29,7 @@ def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str: capture_output=True, ) return result.stdout - except subprocess.CalledProcessError as err: + except Exception as err: raise Exception(f"getting secret {secret_name} in namespace {namespace}:\n{err}") def _install_kubectl(self): @@ -36,5 +39,5 @@ def _install_kubectl(self): version = self.run_command("kubectl version --client", check=True, text=True, capture_output=True) print(f"kubectl installed successfully. Version: {version.stdout.strip()}") - except subprocess.CalledProcessError as err: + except Exception as err: raise Exception(f"installing kubectl:\n{err}") From f9ac85db9fa9e322a43b9e21f3407c8ebb543d35 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 22 Jun 2026 19:11:53 +0300 Subject: [PATCH 09/26] refactor: update import path and simplify get_secret method in Kubectl class --- src/github_actions/common/utils/kubectl.py | 30 +++++----------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index eb667a4..c12e159 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -1,4 +1,4 @@ -from ..utils.cmd import BaseCommand, CMDInterface +from github_actions.common.utils.cmd import BaseCommand, CMDInterface class Kubectl(BaseCommand, CMDInterface): @@ -6,38 +6,22 @@ def __init__(self, kubectl_download_url="https://dl.k8s.io/release/v1.36.2/bin/l self.kubectl_download_url = kubectl_download_url self._install_kubectl() - def execute(self): - self.run() - - def run(self): - print("Kubectl is ready to use.") - - def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str: + def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str | None: try: print(f"Getting secret {secret_name} in namespace {namespace}.") - result = self.run_command( - f"kubectl get secret {secret_name} --namespace {namespace} --output yaml", check=True, text=True, capture_output=True + return self.run_command( + f"kubectl get secret {secret_name} --namespace {namespace} --output yaml | yq '.data.password'", capture_output=True ) - print(f"Secret {secret_name} retrieved successfully. Extracting data.") - # parse yaml output and extract the password field, decode it from base64 - result = self.run_command( - f"yq '.data.{secret_path} | @base64d'", - input=result.stdout, - check=True, - text=True, - capture_output=True, - ) - return result.stdout except Exception as err: raise Exception(f"getting secret {secret_name} in namespace {namespace}:\n{err}") def _install_kubectl(self): print("Installing kubectl.") try: - self.run_command(f"bash -s -- {self.kubectl_download_url}", check=True, text=True, input="") + self.run_command(f"bash -s -- {self.kubectl_download_url}") - version = self.run_command("kubectl version --client", check=True, text=True, capture_output=True) - print(f"kubectl installed successfully. Version: {version.stdout.strip()}") + version = self.run_command("kubectl version --client", capture_output=True) + print(f"kubectl installed successfully. Version: {version}") except Exception as err: raise Exception(f"installing kubectl:\n{err}") From 046f12466dafab9418155ceb062148f76badefeb Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 22 Jun 2026 19:16:25 +0300 Subject: [PATCH 10/26] refactor: restructure Kubectl class by adding execute method and adjusting run method --- src/github_actions/common/utils/kubectl.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index c12e159..59cc183 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -3,9 +3,16 @@ class Kubectl(BaseCommand, CMDInterface): def __init__(self, kubectl_download_url="https://dl.k8s.io/release/v1.36.2/bin/linux/amd64/kubectl"): + super().__init__(None) self.kubectl_download_url = kubectl_download_url + self.run() + + def run(self): self._install_kubectl() + def execute(self): + return self.run() + def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str | None: try: print(f"Getting secret {secret_name} in namespace {namespace}.") From ec80c08d8f1c3b849f40cc60f144e1a39bfe82eb Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Fri, 26 Jun 2026 16:54:25 +0300 Subject: [PATCH 11/26] feat: add context parameter to Kubectl class and update get_secret method to use it --- src/github_actions/common/utils/kubectl.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index 59cc183..024f2d1 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -2,9 +2,10 @@ class Kubectl(BaseCommand, CMDInterface): - def __init__(self, kubectl_download_url="https://dl.k8s.io/release/v1.36.2/bin/linux/amd64/kubectl"): + def __init__(self, context=None, kubectl_download_url="https://dl.k8s.io/release/v1.36.2/bin/linux/amd64/kubectl"): super().__init__(None) self.kubectl_download_url = kubectl_download_url + self.context = context self.run() def run(self): @@ -16,8 +17,9 @@ def execute(self): def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str | None: try: print(f"Getting secret {secret_name} in namespace {namespace}.") + context_flag = f"--context {self.context}" if self.context else "" return self.run_command( - f"kubectl get secret {secret_name} --namespace {namespace} --output yaml | yq '.data.password'", capture_output=True + f"kubectl get secret {secret_name} --namespace {namespace} {context_flag} --output yaml | yq '.data.password'", capture_output=True ) except Exception as err: From bd6f75c7e2d20a20bcb389e71de9cd3ca4e9a582 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Fri, 26 Jun 2026 17:33:27 +0300 Subject: [PATCH 12/26] refactor: enhance get_secret method by adding command logging and context retrieval --- src/github_actions/common/utils/kubectl.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index 024f2d1..b43896b 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -18,9 +18,12 @@ def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str try: print(f"Getting secret {secret_name} in namespace {namespace}.") context_flag = f"--context {self.context}" if self.context else "" - return self.run_command( - f"kubectl get secret {secret_name} --namespace {namespace} {context_flag} --output yaml | yq '.data.password'", capture_output=True - ) + + print(f"contexts list: {self.run_command('kubectl config get-contexts', capture_output=True)}") + + command = f"kubectl get secret {secret_name} --namespace {namespace} {context_flag} --output yaml | yq '.data.password'" + print(f"Executing command: {command}") + return self.run_command(command, capture_output=True) except Exception as err: raise Exception(f"getting secret {secret_name} in namespace {namespace}:\n{err}") From 2fe0e4041940624d6a4eebaacf6fbf18a4a32471 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Fri, 26 Jun 2026 17:46:26 +0300 Subject: [PATCH 13/26] feat: add RMKClusterSwitchCommand for switching RMK cluster context --- src/github_actions/common/__init__.py | 3 ++- src/github_actions/common/actions/init_project.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/github_actions/common/__init__.py b/src/github_actions/common/__init__.py index 120e649..177df46 100644 --- a/src/github_actions/common/__init__.py +++ b/src/github_actions/common/__init__.py @@ -1,6 +1,6 @@ # github_actions/common/__init__.py -from .actions.init_project import GETTenant, ProjectInitializer, RMKConfigInitCommand, GetRootDomain +from .actions.init_project import GETTenant, ProjectInitializer, RMKConfigInitCommand, GetRootDomain, RMKClusterSwitchCommand from .credentials.cluster_provider_credentials import ( AWSConfig, AzureConfig, ClusterProviders, Credentials, EnvironmentConfig, GCPConfig ) @@ -46,6 +46,7 @@ "KeycloakClientInfoFetcher", "ProjectInitializer", "RMKConfigInitCommand", + "RMKClusterSwitchCommand", "RMKInstaller", "S3BucketManager", "SlackNotifier", diff --git a/src/github_actions/common/actions/init_project.py b/src/github_actions/common/actions/init_project.py index 7e2f6ec..f506ee2 100644 --- a/src/github_actions/common/actions/init_project.py +++ b/src/github_actions/common/actions/init_project.py @@ -39,6 +39,20 @@ def run(self): self.run_command(f"rmk config init --cluster-provider={self.cluster_provider} --progress-bar=false") +class RMKClusterSwitchCommand(BaseCommand, CMDInterface): + def __init__(self, force: bool = False): + super().__init__(environment="") + self.force = force + + def execute(self): + self.run() + + def run(self): + """Switch RMK cluster context.""" + force_flag = "--force" if self.force else "" + self.run_command(f"rmk cluster switch {force_flag}") + + class GETTenant(BaseCommand, CMDInterface): def __init__(self, environment: str): super().__init__(environment) From 6b17a76c810b4806250163c5819a448b9a9ce780 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Fri, 26 Jun 2026 17:58:56 +0300 Subject: [PATCH 14/26] fix: decode base64 password when retrieving secret from Kubernetes --- src/github_actions/common/utils/kubectl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index b43896b..ff7801d 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -21,7 +21,7 @@ def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str print(f"contexts list: {self.run_command('kubectl config get-contexts', capture_output=True)}") - command = f"kubectl get secret {secret_name} --namespace {namespace} {context_flag} --output yaml | yq '.data.password'" + command = f"kubectl get secret {secret_name} --namespace {namespace} {context_flag} --output yaml | yq '.data.password | @base64d'" print(f"Executing command: {command}") return self.run_command(command, capture_output=True) From ed6f8cc6f31ffc0c5c034dddb10fcd62810330bd Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Fri, 26 Jun 2026 18:25:08 +0300 Subject: [PATCH 15/26] refactor: add logging for access token retrieval in KeycloakClientInfoFetcher --- src/github_actions/common/utils/keycloak.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index baa7405..3f3ec0b 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -44,6 +44,9 @@ def _get_access_token(self) -> str: token_response.raise_for_status() access_token = token_response.json().get("access_token") + + # TODO: remove logging before merge! + print(f"Retrieved access token from Keycloak token endpoint. access_token: {access_token}") if not access_token: raise ValueError("access_token was not returned by Keycloak token endpoint") return access_token From 8365d9662006d4e08ba96ba61430ada6fb1d550c Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Fri, 26 Jun 2026 18:29:31 +0300 Subject: [PATCH 16/26] refactor: update access token retrieval logging in KeycloakClientInfoFetcher --- src/github_actions/common/utils/keycloak.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index 3f3ec0b..c17b8f3 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -43,10 +43,12 @@ def _get_access_token(self) -> str: ) token_response.raise_for_status() - access_token = token_response.json().get("access_token") + response_json = token_response.json() # TODO: remove logging before merge! - print(f"Retrieved access token from Keycloak token endpoint. access_token: {access_token}") + print(f"Retrieved: {response_json}") + + access_token = response_json.get("access_token") if not access_token: raise ValueError("access_token was not returned by Keycloak token endpoint") return access_token From 5826e4a063eba96905920d76fac7499e401ccbf4 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Fri, 26 Jun 2026 18:32:49 +0300 Subject: [PATCH 17/26] refactor: add logging for access token retrieval in KeycloakClientInfoFetcher --- src/github_actions/common/utils/keycloak.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index c17b8f3..b382b36 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -49,6 +49,9 @@ def _get_access_token(self) -> str: print(f"Retrieved: {response_json}") access_token = response_json.get("access_token") + + # TODO: remove logging before merge! + print(f"Access token: {access_token[:10]}...") # Print only the first 10 characters for security if not access_token: raise ValueError("access_token was not returned by Keycloak token endpoint") return access_token From b241cb2a1556c0f5edc3c6ae5269125ef5f865f7 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Fri, 26 Jun 2026 18:42:52 +0300 Subject: [PATCH 18/26] refactor: streamline access token request data formatting in KeycloakClientInfoFetcher --- src/github_actions/common/utils/keycloak.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index b382b36..21e7d43 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -1,3 +1,4 @@ +from urllib.parse import urlencode import requests @@ -30,15 +31,20 @@ def get_client_info(self) -> list[dict]: return clients_response.json() def _get_access_token(self) -> str: - token_response = requests.post( - f"{self.base_url}/realms/{self.token_realm}/protocol/openid-connect/token", - headers={"content-type": "application/x-www-form-urlencoded"}, - data={ + + data = urlencode( + { "client_id": self.client_id, "username": self.username, "grant_type": "password", "password": self.password, - }, + } + ) + + token_response = requests.post( + f"{self.base_url}/realms/{self.token_realm}/protocol/openid-connect/token", + headers={"content-type": "application/x-www-form-urlencoded"}, + data=data, timeout=30, ) token_response.raise_for_status() From 83068cda1e40136284f1fc0e72306b724b875873 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 29 Jun 2026 12:55:34 +0300 Subject: [PATCH 19/26] fix: validate access token to ensure it is not masked --- src/github_actions/common/utils/keycloak.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index 21e7d43..69e4d10 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -60,4 +60,7 @@ def _get_access_token(self) -> str: print(f"Access token: {access_token[:10]}...") # Print only the first 10 characters for security if not access_token: raise ValueError("access_token was not returned by Keycloak token endpoint") + if access_token == "***": + raise ValueError("access_token is masked and not valid") + return access_token From d6f30ea181c84665763d59476f837563b9dedad5 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 29 Jun 2026 17:22:41 +0300 Subject: [PATCH 20/26] fix: validate access token length to ensure it is not masked --- src/github_actions/common/utils/keycloak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index 69e4d10..dbfdade 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -60,7 +60,7 @@ def _get_access_token(self) -> str: print(f"Access token: {access_token[:10]}...") # Print only the first 10 characters for security if not access_token: raise ValueError("access_token was not returned by Keycloak token endpoint") - if access_token == "***": + if len(access_token) < 10: # Assuming a valid token should be longer than 10 characters raise ValueError("access_token is masked and not valid") return access_token From 27bbe3e7dc379e0587d06ce7f489e2b763c76ac9 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 29 Jun 2026 17:41:50 +0300 Subject: [PATCH 21/26] refactor: simplify URL construction and logging for client info retrieval in KeycloakClientInfoFetcher --- src/github_actions/common/utils/keycloak.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index dbfdade..ff5fe4b 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -21,8 +21,11 @@ def __init__( def get_client_info(self) -> list[dict]: access_token = self._get_access_token() + url = f"{self.base_url}/admin/realms/{self.clients_realm}/clients" + print(f"Fetching client info from: {url} with access token: {access_token[:10]}...") # Print only the first 10 characters for security + clients_response = requests.get( - f"{self.base_url}/admin/realms/{self.clients_realm}/clients", + url, headers={"Authorization": f"Bearer {access_token}"}, params={"clientId": self.client_id}, timeout=30, @@ -56,8 +59,6 @@ def _get_access_token(self) -> str: access_token = response_json.get("access_token") - # TODO: remove logging before merge! - print(f"Access token: {access_token[:10]}...") # Print only the first 10 characters for security if not access_token: raise ValueError("access_token was not returned by Keycloak token endpoint") if len(access_token) < 10: # Assuming a valid token should be longer than 10 characters From cd12b4d661141c9cab5c0273056377cb72d28e98 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 29 Jun 2026 17:45:58 +0300 Subject: [PATCH 22/26] fix: add debug logging for access token length in KeycloakClientInfoFetcher --- src/github_actions/common/utils/keycloak.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index ff5fe4b..7c77e47 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -58,6 +58,7 @@ def _get_access_token(self) -> str: print(f"Retrieved: {response_json}") access_token = response_json.get("access_token") + print(f"Access token length: {len(access_token) if access_token else 'None'}") # Print the length of the token for debugging if not access_token: raise ValueError("access_token was not returned by Keycloak token endpoint") From b35a93506d6340ad7ed850b4ce1b273ba78bc5e1 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 29 Jun 2026 17:58:45 +0300 Subject: [PATCH 23/26] refactor: update logging to display client_id instead of access token in KeycloakClientInfoFetcher --- src/github_actions/common/utils/keycloak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index 7c77e47..8f88848 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -22,7 +22,7 @@ def __init__( def get_client_info(self) -> list[dict]: access_token = self._get_access_token() url = f"{self.base_url}/admin/realms/{self.clients_realm}/clients" - print(f"Fetching client info from: {url} with access token: {access_token[:10]}...") # Print only the first 10 characters for security + print(f"Fetching client info from: {url} for client_id: {self.client_id}") # Print only the first 10 characters for security clients_response = requests.get( url, From d87492226cf7c9197b1ee73b813fffffd9120034 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Mon, 29 Jun 2026 18:03:50 +0300 Subject: [PATCH 24/26] fix: update admin_client_id usage in KeycloakClientInfoFetcher --- src/github_actions/common/utils/keycloak.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index 8f88848..dd8913e 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -7,6 +7,7 @@ def __init__( self, keycloak_url: str, username: str, + admin_client_id: str, client_id: str, password: str, clients_realm: str, @@ -14,6 +15,7 @@ def __init__( ): self.base_url = keycloak_url.rstrip("/") self.username = username + self.admin_client_id = admin_client_id self.client_id = client_id self.password = password self.token_realm = token_realm @@ -34,10 +36,9 @@ def get_client_info(self) -> list[dict]: return clients_response.json() def _get_access_token(self) -> str: - data = urlencode( { - "client_id": self.client_id, + "client_id": self.admin_client_id, "username": self.username, "grant_type": "password", "password": self.password, From 429d14769bce83f2f675c9579037976976459822 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Wed, 1 Jul 2026 19:16:10 +0300 Subject: [PATCH 25/26] fix: enhance kubectl installation check and logging --- src/github_actions/common/utils/kubectl.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/github_actions/common/utils/kubectl.py b/src/github_actions/common/utils/kubectl.py index ff7801d..e702469 100644 --- a/src/github_actions/common/utils/kubectl.py +++ b/src/github_actions/common/utils/kubectl.py @@ -29,11 +29,24 @@ def get_secret(self, secret_name: str, namespace: str, secret_path: str) -> str raise Exception(f"getting secret {secret_name} in namespace {namespace}:\n{err}") def _install_kubectl(self): - print("Installing kubectl.") + print("Checking kubectl installation.") try: + if self._is_kubectl_installed(): + version = self.run_command("kubectl version --client", capture_output=True) + print(f"kubectl already installed. Version: {version}") + return + + print("Installing kubectl.") self.run_command(f"bash -s -- {self.kubectl_download_url}") version = self.run_command("kubectl version --client", capture_output=True) print(f"kubectl installed successfully. Version: {version}") except Exception as err: raise Exception(f"installing kubectl:\n{err}") + + def _is_kubectl_installed(self) -> bool: + try: + self.run_command("command -v kubectl", capture_output=True) + return True + except ValueError: + return False From 3778c1b4877c4d9d89043c724e8312307bcaf931 Mon Sep 17 00:00:00 2001 From: Bohdan Hrytsenko Date: Thu, 2 Jul 2026 10:47:52 +0300 Subject: [PATCH 26/26] chore: cleanup --- src/github_actions/common/utils/keycloak.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/github_actions/common/utils/keycloak.py b/src/github_actions/common/utils/keycloak.py index dd8913e..b734c1e 100644 --- a/src/github_actions/common/utils/keycloak.py +++ b/src/github_actions/common/utils/keycloak.py @@ -63,7 +63,5 @@ def _get_access_token(self) -> str: if not access_token: raise ValueError("access_token was not returned by Keycloak token endpoint") - if len(access_token) < 10: # Assuming a valid token should be longer than 10 characters - raise ValueError("access_token is masked and not valid") return access_token