diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 441e305..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,19 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. -{ - "name": "Codefresh Support Package", - "image": "mcr.microsoft.com/devcontainers/universal:3", - "onCreateCommand": "curl -fsSL https://deno.land/install.sh | sh -s -- -y", - "customizations": { - "vscode": { - "settings": { - "deno.enable": true, - "deno.lint": true - }, - "extensions": [ - "denoland.vscode-deno", - "davidanson.vscode-markdownlint", - "redhat.vscode-yaml" - ] - } - } -} diff --git a/.gitignore b/.gitignore index 13c73c3..a2dc396 100644 --- a/.gitignore +++ b/.gitignore @@ -1,132 +1,36 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.pyc +*.pyd +*.so + +# Virtual environment +venv/ +.venv/ +env/ + +# IDE specific files +.idea/ +*.iml +.vscode/ + +# Jupyter Notebook +.ipynb_checkpoints + +# Distribution / build files +dist/ +build/ +*.egg-info/ + # Logs -logs *.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files +# ignore env .env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* +# OS generated files .DS_Store -bin \ No newline at end of file +Thumbs.db + +# Auto Generated Version +_version.py \ No newline at end of file diff --git a/codresh-support-package.env b/codresh-support-package.env new file mode 100644 index 0000000..0309f0c --- /dev/null +++ b/codresh-support-package.env @@ -0,0 +1,3 @@ +CF_API_KEY= +CF_URL= +USERPROFILE= \ No newline at end of file diff --git a/deno.json b/old/deno.json similarity index 100% rename from deno.json rename to old/deno.json diff --git a/deno.lock b/old/deno.lock similarity index 100% rename from deno.lock rename to old/deno.lock diff --git a/main.js b/old/main.js similarity index 100% rename from main.js rename to old/main.js diff --git a/src/gitops.js b/old/src/gitops.js similarity index 100% rename from src/gitops.js rename to old/src/gitops.js diff --git a/src/index.js b/old/src/index.js similarity index 100% rename from src/index.js rename to old/src/index.js diff --git a/src/logic/codefresh.js b/old/src/logic/codefresh.js similarity index 100% rename from src/logic/codefresh.js rename to old/src/logic/codefresh.js diff --git a/src/logic/core.js b/old/src/logic/core.js similarity index 100% rename from src/logic/core.js rename to old/src/logic/core.js diff --git a/src/logic/k8s.js b/old/src/logic/k8s.js similarity index 100% rename from src/logic/k8s.js rename to old/src/logic/k8s.js diff --git a/src/onprem.js b/old/src/onprem.js similarity index 100% rename from src/onprem.js rename to old/src/onprem.js diff --git a/src/oss.js b/old/src/oss.js similarity index 100% rename from src/oss.js rename to old/src/oss.js diff --git a/src/pipelines.js b/old/src/pipelines.js similarity index 100% rename from src/pipelines.js rename to old/src/pipelines.js diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ee339e1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "cf-support" +dynamic = ["version"] +description = "Codefresh Support Package" +authors = [{ name = "Codefresh Support", email = "support@codefresh.io" }] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "click>=8.1", + "kubernetes>=31.1", + "PyYAML>=6.0", + "requests>=2.32", + "python-dotenv>=1.1.1" +] + +[project.scripts] +cf-support = "main:cli" + +[build-system] +requires = ["setuptools>=80.9", "setuptools_scm[toml]>=8.3"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "src/_version.py" \ No newline at end of file diff --git a/src/commands/__init__.py b/src/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/commands/gitops.py b/src/commands/gitops.py new file mode 100644 index 0000000..bb6483f --- /dev/null +++ b/src/commands/gitops.py @@ -0,0 +1,10 @@ +import click + +@click.command(name='gitops') # Use @click.command() directly +@click.option('-n','--namespace', help='The namespace where the GitOps Runtime is installed') +def gitops_command(namespace): + """Collect data for the Codefresh GitOps Runtime""" + + click.echo(f"Executing pipelines command with filter: {filter if filter else 'none'}") + # Add your core logic for the 'pipelines' command here. + # This might involve calling functions from 'utils.py' or directly performing actions. \ No newline at end of file diff --git a/src/commands/onprem.py b/src/commands/onprem.py new file mode 100644 index 0000000..c62e827 --- /dev/null +++ b/src/commands/onprem.py @@ -0,0 +1,10 @@ +import click + +@click.command(name='onprem') # Use @click.command() directly +@click.option('-n','--namespace', help='The namespace where Codefresh OnPrem is installed') +def onprem_command(namespace): + """Collect data for the Codefresh OnPrem Installation""" + + click.echo(f"Executing pipelines command with filter: {filter if filter else 'none'}") + # Add your core logic for the 'pipelines' command here. + # This might involve calling functions from 'utils.py' or directly performing actions. \ No newline at end of file diff --git a/src/commands/oss.py b/src/commands/oss.py new file mode 100644 index 0000000..c12ef73 --- /dev/null +++ b/src/commands/oss.py @@ -0,0 +1,10 @@ +import click + +@click.command(name='oss') # Use @click.command() directly +@click.option('-n','--namespace', help='The namespace where the OSS ArgoCD is installed') +def oss_command(namespace): + """Collect data for the Open Source ArgoCD""" + + click.echo(f"Executing pipelines command with filter: {filter if filter else 'none'}") + # Add your core logic for the 'pipelines' command here. + # This might involve calling functions from 'utils.py' or directly performing actions. \ No newline at end of file diff --git a/src/commands/pipelines.py b/src/commands/pipelines.py new file mode 100644 index 0000000..bf3d46e --- /dev/null +++ b/src/commands/pipelines.py @@ -0,0 +1,11 @@ +import click + +@click.command(name='pipelines') # Use @click.command() directly +@click.option('-n','--namespace', help='The namespace where the Pipelines Runtime is installed') +@click.option('-r','--runtime', help='The name of the Pipelines Runtime') +def pipelines_command(namespace, runtime): + """Collect data for the Codefresh Pipelines Runtime""" + + click.echo(f"Executing pipelines command with filter: {filter if filter else 'none'}") + # Add your core logic for the 'pipelines' command here. + # This might involve calling functions from 'utils.py' or directly performing actions. \ No newline at end of file diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/account_controller.py b/src/controllers/account_controller.py new file mode 100644 index 0000000..d01c64b --- /dev/null +++ b/src/controllers/account_controller.py @@ -0,0 +1,19 @@ +from utilities.logger_config import setup_logger +import requests + +logger = setup_logger(__name__) + + +class AccountController: + def __init__(self, cf_creds: dict[str, dict[str, str]]) -> None: + logger.debug(f"{self.__class__.__name__} initialized") + self.base_url = cf_creds["base_url"] + self.auth_headers = cf_creds["headers"] + + def get_runtimes(self): + logger.info(f"{self.__class__.__name__} is getting account runtimes") + response = requests.get( + f"{self.base_url}/runtime-environments", + headers=self.auth_headers["headers"], # type: ignore + ) + return response.json() diff --git a/src/controllers/auth_controller.py b/src/controllers/auth_controller.py new file mode 100644 index 0000000..cd124db --- /dev/null +++ b/src/controllers/auth_controller.py @@ -0,0 +1,56 @@ +from utilities.logger_config import setup_logger +import yaml +import os + +logger = setup_logger(__name__) + + +class AuthController: + def __init__(self, env_token: str | None, env_url: str | None) -> None: + logger.debug(f"{self.__class__.__name__} initialized") + self.env_token = env_token + self.env_url = env_url + + def get_cf_credentials( + self, + ) -> dict[str, dict[str, str] | str] | None: + logger.info(f"{self.__class__.__name__} is getting CF credentials") + env_token = self.env_token + env_url = self.env_url + cf_credentials: dict[str, dict[str, str] | str] | None = None + + if env_token and env_url: + auth_header: dict[str, str] = {"Authorization": env_token} + + cf_credentials = { + "headers": auth_header, + "base_url": f"{env_url}/api", + } + + else: + config_path = ( + f"{os.getenv('USERPROFILE')}/.cfconfig" + if os.name == "nt" + else f"{os.getenv('HOME')}/.cfconfig" + ) + + try: + with open(config_path, "r") as config_file: + config = yaml.safe_load(config_file) + + current_context = config["contexts"].get(config["current-context"]) + except (FileNotFoundError, PermissionError, yaml.YAMLError): + current_context = None + + if current_context: + context_token = current_context["token"] + context_url = current_context["url"] + + if context_token and context_url: + auth_header = {"Authorization": context_token} + cf_credentials = { + "headers": auth_header, + "base_url": f"{context_url}/api", + } + + return cf_credentials diff --git a/src/controllers/k8s_controller.py b/src/controllers/k8s_controller.py new file mode 100644 index 0000000..80d4ab1 --- /dev/null +++ b/src/controllers/k8s_controller.py @@ -0,0 +1,143 @@ +from typing import Dict, Optional, Any +from kubernetes import client, config # type: ignore +from kubernetes.client.rest import ApiException # type: ignore + + +class K8sController: + def __init__(self): + """Initialize Kubernetes client with auto-detected configuration.""" + + try: + # Try to load in-cluster config first, then kubeconfig + config.load_incluster_config() # type: ignore + except config.ConfigException: + config.load_kube_config() # type: ignore + + self.core_api = client.CoreV1Api() + self.apps_api = client.AppsV1Api() + self.batch_api = client.BatchV1Api() + self.storage_api = client.StorageV1Api() + self.crd_api = client.ApiextensionsV1Api() + self.custom_objects_api = client.CustomObjectsApi() + + def select_namespace(self) -> str: + """Interactive namespace selection.""" + namespaces: list[str] = [ns.metadata.name for ns in self.core_api.list_namespace().items] # type: ignore + + for index, namespace in enumerate(namespaces, 1): + print(f"{index}. {namespace}") + + while True: + try: + selection = int(input('\nWhich Namespace are we using? (Number): ')) + if 1 <= selection <= len(namespaces): + return namespaces[selection - 1] + else: + print('Invalid selection. Please enter a number corresponding to one of the listed namespaces.') + except ValueError: + print('Invalid selection. Please enter a number corresponding to one of the listed namespaces.') + + def get_pod_logs(self, pod: Dict[str, Any]) -> Dict[str, str]: + """Get logs for all containers in a pod.""" + pod_name = pod['metadata']['name'] + namespace = pod['metadata']['namespace'] + containers = [container['name'] for container in pod['spec']['containers']] + + logs: Dict[str, str] = {} + for container in containers: + try: + logs[container] = self.core_api.read_namespaced_pod_log(name=pod_name, namespace=namespace, container=container, timestamps=True ) # type: ignore + except ApiException as error: + logs[container] = str(error) + + return logs + + def _get_crd(self, crd_type: str, namespace: str) -> Optional[Dict[str, Any]]: + """Get Custom Resource Definition objects.""" + try: + crd = self.crd_api.read_custom_resource_definition(crd_type) + + # Find served version + served_version = next( + (v.name for v in crd.spec.versions if v.served), + None + ) + + if not served_version: + return None + + # Get custom resources + response = self.custom_objects_api.list_namespaced_custom_object( + group=crd.spec.group, + version=served_version, + namespace=namespace, + plural=crd.spec.names.plural + ) + + return response + except ApiException: + return None + + def _get_sorted_events(self, namespace: str) -> client.V1EventList: + """Get events sorted by creation timestamp.""" + events = self.core_api.list_namespaced_event(namespace) + + # Sort events by creation timestamp + events.items.sort( + key=lambda event: event.metadata.creation_timestamp + ) + + return events + + def fetch_all_resources(self, namespace: str) -> Dict[str, Any]: + """Fetch all Kubernetes resources for a namespace.""" + + k8s_resource_types = { + 'configmaps': lambda: self.core_api.list_namespaced_config_map(namespace), # type: ignore + 'cronjobs.batch': lambda: self.batch_api.list_namespaced_cron_job(namespace), # type: ignore + 'daemonsets.apps': lambda: self.apps_api.list_namespaced_daemon_set(namespace), # type: ignore + 'deployments.apps': lambda: self.apps_api.list_namespaced_deployment(namespace), # type: ignore + 'events.k8s.io': lambda: self._get_sorted_events(namespace), # type: ignore + 'jobs.batch': lambda: self.batch_api.list_namespaced_job(namespace), # type: ignore + 'nodes': lambda: self.core_api.list_node(), # type: ignore + 'pods': lambda: self.core_api.list_namespaced_pod(namespace), # type: ignore + 'serviceaccounts': lambda: self.core_api.list_namespaced_service_account(namespace), # type: ignore + 'services': lambda: self.core_api.list_namespaced_service(namespace), # type: ignore + 'statefulsets.apps': lambda: self.apps_api.list_namespaced_stateful_set(namespace), # type: ignore + 'persistentvolumeclaims': lambda: self.core_api.list_namespaced_persistent_volume_claim( # type: ignore + namespace, label_selector='io.codefresh.accountName' + ), + 'persistentvolumes': lambda: self.core_api.list_persistent_volume( # type: ignore + label_selector='io.codefresh.accountName' + ), + 'storageclasses.storage.k8s.io': lambda: self.storage_api.list_storage_class(), # type: ignore + + # Codefresh CRDs + 'products.codefresh.io': lambda: self._get_crd('products.codefresh.io', namespace), + 'promotionflows.codefresh.io': lambda: self._get_crd('promotionflows.codefresh.io', namespace), + 'promotionpolicies.codefresh.io': lambda: self._get_crd('promotionpolicies.codefresh.io', namespace), + 'promotiontemplates.codefresh.io': lambda: self._get_crd('promotiontemplates.codefresh.io', namespace), + 'restrictedgitsources.codefresh.io': lambda: self._get_crd('restrictedgitsources.codefresh.io', namespace), + + # ArgoProj CRDs + 'analysisruns.argoproj.io': lambda: self._get_crd('analysisruns.argoproj.io', namespace), + 'analysistemplates.argoproj.io': lambda: self._get_crd('analysistemplates.argoproj.io', namespace), + 'applications.argoproj.io': lambda: self._get_crd('applications.argoproj.io', namespace), + 'applicationsets.argoproj.io': lambda: self._get_crd('applicationsets.argoproj.io', namespace), + 'appprojects.argoproj.io': lambda: self._get_crd('appprojects.argoproj.io', namespace), + 'eventbus.argoproj.io': lambda: self._get_crd('eventbus.argoproj.io', namespace), + 'eventsources.argoproj.io': lambda: self._get_crd('eventsources.argoproj.io', namespace), + 'experiments.argoproj.io': lambda: self._get_crd('experiments.argoproj.io', namespace), + 'rollouts.argoproj.io': lambda: self._get_crd('rollouts.argoproj.io', namespace), + 'sensors.argoproj.io': lambda: self._get_crd('sensors.argoproj.io', namespace), + } + resources = {} + + for resource_type, fetch_func in k8s_resource_types.items(): + try: + print(f"Fetching {resource_type}...") + resources[resource_type] = fetch_func() + except ApiException as e: + resources[resource_type] = None + + return resources \ No newline at end of file diff --git a/src/controllers/runtime_controller.py b/src/controllers/runtime_controller.py new file mode 100644 index 0000000..772388f --- /dev/null +++ b/src/controllers/runtime_controller.py @@ -0,0 +1,21 @@ +from utilities.logger_config import setup_logger +import requests + +logger = setup_logger(__name__) + + +class RuntimeController: + def __init__(self, cf_creds: dict[str, dict[str, str]]) -> None: + logger.debug(f"{self.__class__.__name__} initialized") + self.base_url = cf_creds["base_url"] + self.auth_headers = cf_creds["headers"] + + def get_spec(self, runtime_name: str): + logger.info( + f"{self.__class__.__name__} is getting runtime spec for runtime '{runtime_name}'" + ) + response = requests.get( + f"{self.base_url}/runtime-environments/{runtime_name}", + headers=self.auth_headers, + ) + return response.json() diff --git a/src/controllers/system_controller.py b/src/controllers/system_controller.py new file mode 100644 index 0000000..36fb67e --- /dev/null +++ b/src/controllers/system_controller.py @@ -0,0 +1,45 @@ +from utilities.logger_config import setup_logger +import requests + +logger = setup_logger(__name__) + + +class SystemController: + def __init__(self, cf_creds: dict[str, dict[str, str]]) -> None: + logger.debug(f"{self.__class__.__name__} initialized") + self.base_url = cf_creds["base_url"] + self.auth_headers = cf_creds["headers"] + + def get_all_accounts(self): + logger.info(f"{self.__class__.__name__} is getting all accounts") + response = requests.get( + f"{self.base_url}/admin/accounts", + headers=self.auth_headers, + ) + return response.json() + + def get_all_runtimes(self): + logger.info(f"{self.__class__.__name__} is getting all runtimes (cross-account)") + response = requests.get( + f"{self.base_url}/admin/runtime-environments", + headers=self.auth_headers, + ) + return response.json() + + def get_feature_flags(self): + logger.info(f"{self.__class__.__name__} is getting feature flags") + response = requests.get( + f"{self.base_url}/admin/features", + headers=self.auth_headers, + ) + return response.json() + + def get_total_users(self): + logger.info(f"{self.__class__.__name__} is getting total users (cross-account)") + response = requests.get( + f"{self.base_url}/admin/user?limit=1&page=1", + headers=self.auth_headers, + ) + users = response.json() + + return {"totalUsers": users["total"]} diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..e52869d --- /dev/null +++ b/src/main.py @@ -0,0 +1,41 @@ +from commands import pipelines, gitops, onprem, oss +from controllers.account_controller import AccountController +from controllers.auth_controller import AuthController +from controllers.runtime_controller import RuntimeController +from controllers.system_controller import SystemController +from dotenv import load_dotenv +from utilities.logger_config import setup_logger +import click +import os + +load_dotenv() +logger = setup_logger(__name__) + + +logger.info("Starting CF Support CLI") +env_token = os.getenv("CF_API_KEY") +env_url = os.getenv("CF_URL") +auth_controller = AuthController(env_token, env_url) + +try: + from _version import version as __version__ +except ImportError: + __version__ = "0.0.0+dev.uninstalled" + +@click.group() +@click.version_option(version=__version__, prog_name="cf-support") +def cli(): + """Codefresh Support Package + + Tool to gather information for Codefresh Support + """ + pass + +cli.add_command(pipelines.pipelines_command) +cli.add_command(gitops.gitops_command) +cli.add_command(onprem.onprem_command) +cli.add_command(oss.oss_command) + + +if __name__ == "__main__": + cli() diff --git a/src/models/STUB b/src/models/STUB new file mode 100644 index 0000000..e69de29 diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utilities/logger_config.py b/src/utilities/logger_config.py new file mode 100644 index 0000000..840091c --- /dev/null +++ b/src/utilities/logger_config.py @@ -0,0 +1,28 @@ +import logging +import os + +LOG_DIR = "logs" +LOG_FILE = "cli.log" +os.makedirs(LOG_DIR, exist_ok=True) + + +def setup_logger(name: str) -> logging.Logger: + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + + if not logger.hasHandlers(): + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(name)s.%(funcName)s() - %(message)s" + ) + + file_handler = logging.FileHandler(os.path.join(LOG_DIR, LOG_FILE)) + file_handler.setFormatter(formatter) + file_handler.setLevel(logging.DEBUG) + + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(formatter) + stream_handler.setLevel(logging.INFO) + + logger.addHandler(file_handler) + + return logger diff --git a/src/utilities/utility.py b/src/utilities/utility.py new file mode 100644 index 0000000..efc307c --- /dev/null +++ b/src/utilities/utility.py @@ -0,0 +1,31 @@ +from utilities.logger_config import setup_logger +import tarfile +import yaml +import os + +logger = setup_logger(__name__) + + +class Utility: + def __init__(self) -> None: + pass + + def write_yaml_to_file(self, py_obj: object, filename: str): + with open(f"{filename}.yaml", "w") as f: + yaml.dump(py_obj, f, sort_keys=False) + print("Written to file successfully") + + def create_tar_gz(self, output_filename: str, source_dir: str): + if not os.path.isdir(source_dir): + raise NotADirectoryError(f"{source_dir} is not a valid directory") + + with tarfile.open(output_filename, "w:gz") as tar: + tar.add(source_dir, arcname=os.path.basename(source_dir)) + + def prepare_package(self, dir_path: str): + compressed_support_package = f"{dir_path}.tar.gz" + + logger.info(f"{self.__class__.__name__} Preparing the support package...") + + def process_dat(self): + pass \ No newline at end of file