Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9fde377
feat: add KeycloakClientInfoFetcher for Keycloak client information r…
gritsenkob Jun 19, 2026
2b88765
feat: implement Kubectl class for managing kubectl installation and s…
gritsenkob Jun 22, 2026
03151bb
feat: refactor Kubectl class to simplify initialization and installat…
gritsenkob Jun 22, 2026
9c33ae0
feat: add Kubectl to module exports in __init__.py
gritsenkob Jun 22, 2026
645dd9f
refactor: move install_kubectl method to a private method and update …
gritsenkob Jun 22, 2026
6d6aa97
feat: enhance get_secret method with additional logging and fix yq co…
gritsenkob Jun 22, 2026
bc5c8b4
feat: refactor Kubectl class to utilize run_command for subprocess calls
gritsenkob Jun 22, 2026
304e81c
refactor: remove unused import and simplify exception handling in Kub…
gritsenkob Jun 22, 2026
f9ac85d
refactor: update import path and simplify get_secret method in Kubect…
gritsenkob Jun 22, 2026
046f124
refactor: restructure Kubectl class by adding execute method and adju…
gritsenkob Jun 22, 2026
ec80c08
feat: add context parameter to Kubectl class and update get_secret me…
gritsenkob Jun 26, 2026
bd6f75c
refactor: enhance get_secret method by adding command logging and con…
gritsenkob Jun 26, 2026
2fe0e40
feat: add RMKClusterSwitchCommand for switching RMK cluster context
gritsenkob Jun 26, 2026
6b17a76
fix: decode base64 password when retrieving secret from Kubernetes
gritsenkob Jun 26, 2026
ed6f8cc
refactor: add logging for access token retrieval in KeycloakClientInf…
gritsenkob Jun 26, 2026
8365d96
refactor: update access token retrieval logging in KeycloakClientInfo…
gritsenkob Jun 26, 2026
5826e4a
refactor: add logging for access token retrieval in KeycloakClientInf…
gritsenkob Jun 26, 2026
b241cb2
refactor: streamline access token request data formatting in Keycloak…
gritsenkob Jun 26, 2026
83068cd
fix: validate access token to ensure it is not masked
gritsenkob Jun 29, 2026
d6f30ea
fix: validate access token length to ensure it is not masked
gritsenkob Jun 29, 2026
27bbe3e
refactor: simplify URL construction and logging for client info retri…
gritsenkob Jun 29, 2026
cd12b4d
fix: add debug logging for access token length in KeycloakClientInfoF…
gritsenkob Jun 29, 2026
b35a935
refactor: update logging to display client_id instead of access token…
gritsenkob Jun 29, 2026
d874922
fix: update admin_client_id usage in KeycloakClientInfoFetcher
gritsenkob Jun 29, 2026
429d147
fix: enhance kubectl installation check and logging
gritsenkob Jul 1, 2026
3778c1b
chore: cleanup
gritsenkob Jul 2, 2026
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
7 changes: 6 additions & 1 deletion src/github_actions/common/__init__.py
Original file line number Diff line number Diff line change
@@ -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
)
Expand All @@ -18,6 +18,8 @@
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


__all__ = [
Expand All @@ -41,11 +43,14 @@
"GETTenant",
"GitHubContext",
"GitHubOutput",
"KeycloakClientInfoFetcher",
"ProjectInitializer",
"RMKConfigInitCommand",
"RMKClusterSwitchCommand",
"RMKInstaller",
"S3BucketManager",
"SlackNotifier",
"KodjinCLIInstaller",
"Kubectl",
"GetRootDomain",
]
14 changes: 14 additions & 0 deletions src/github_actions/common/actions/init_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 67 additions & 0 deletions src/github_actions/common/utils/keycloak.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from urllib.parse import urlencode
import requests


class KeycloakClientInfoFetcher:
def __init__(
self,
keycloak_url: str,
username: str,
admin_client_id: str,
client_id: str,
password: str,
clients_realm: str,
token_realm: str = "master",
):
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
self.clients_realm = clients_realm

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} for client_id: {self.client_id}") # Print only the first 10 characters for security

clients_response = requests.get(
url,
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:
data = urlencode(
{
"client_id": self.admin_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()

response_json = token_response.json()

# TODO: remove logging before merge!
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")

return access_token
52 changes: 52 additions & 0 deletions src/github_actions/common/utils/kubectl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from github_actions.common.utils.cmd import BaseCommand, CMDInterface


class Kubectl(BaseCommand, CMDInterface):
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):
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}.")
context_flag = f"--context {self.context}" if self.context else ""

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 | @base64d'"
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}")

def _install_kubectl(self):
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
Loading