From b81782dabddc1ccf94b06ed1d2614918c64b60cd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Jan 2026 07:25:24 +0000 Subject: [PATCH] Add deployment context detection for targeted user guidance Adds utility to detect whether user is running via: - CLI (command line) - Helm chart (Robusta) - Helm chart (Holmes standalone) This enables showing deployment-specific guidance for environment variable configuration and other setup instructions. Key features: - DeploymentContext class with method and chart_type detection - get_env_var_guidance() helper for generating targeted help messages - Detection based on PLAYBOOKS_CONFIG_FILE_PATH, INSTALLATION_NAMESPACE, RELEASE_NAME, HOLMES_ENABLED, and HOLMES_STANDALONE env vars - Different guidance for CLI users vs Robusta Helm vs Holmes Helm https://claude.ai/code/session_01DuWGiUa4DB1FBPG5mLmqhD --- src/robusta/core/model/deployment_context.py | 264 +++++++++++++++++++ tests/test_deployment_context.py | 175 ++++++++++++ 2 files changed, 439 insertions(+) create mode 100644 src/robusta/core/model/deployment_context.py create mode 100644 tests/test_deployment_context.py diff --git a/src/robusta/core/model/deployment_context.py b/src/robusta/core/model/deployment_context.py new file mode 100644 index 000000000..49baa4627 --- /dev/null +++ b/src/robusta/core/model/deployment_context.py @@ -0,0 +1,264 @@ +""" +Utilities for detecting deployment context (CLI vs Helm, Robusta vs Holmes). + +This module provides functions to detect how Robusta/Holmes is deployed and +generate appropriate guidance messages for users based on their deployment method. +""" + +import os +from enum import Enum +from typing import Optional + + +class DeploymentMethod(Enum): + """How the application is deployed.""" + CLI = "cli" + HELM = "helm" + UNKNOWN = "unknown" + + +class ChartType(Enum): + """Which Helm chart is being used (if deployed via Helm).""" + ROBUSTA = "robusta" + HOLMES = "holmes" + UNKNOWN = "unknown" + + +class DeploymentContext: + """Detected deployment context information.""" + + def __init__( + self, + method: DeploymentMethod, + chart_type: ChartType, + release_name: Optional[str] = None, + namespace: Optional[str] = None, + ): + self.method = method + self.chart_type = chart_type + self.release_name = release_name + self.namespace = namespace + + @property + def is_cli(self) -> bool: + return self.method == DeploymentMethod.CLI + + @property + def is_helm(self) -> bool: + return self.method == DeploymentMethod.HELM + + @property + def is_robusta_chart(self) -> bool: + return self.chart_type == ChartType.ROBUSTA + + @property + def is_holmes_chart(self) -> bool: + return self.chart_type == ChartType.HOLMES + + +def detect_deployment_context() -> DeploymentContext: + """ + Detect the current deployment context based on environment variables. + + Detection logic: + - CLI: No PLAYBOOKS_CONFIG_FILE_PATH and no INSTALLATION_NAMESPACE set + (or INSTALLATION_NAMESPACE is the default "robusta" without other indicators) + - Helm (Robusta): Has PLAYBOOKS_CONFIG_FILE_PATH set to /etc/robusta/... path + - Helm (Holmes standalone): Has HOLMES_ENABLED but no Robusta-specific paths, + or explicitly marked via HOLMES_STANDALONE env var + + Returns: + DeploymentContext with detected method and chart type + """ + playbooks_config_path = os.environ.get("PLAYBOOKS_CONFIG_FILE_PATH") + installation_namespace = os.environ.get("INSTALLATION_NAMESPACE") + release_name = os.environ.get("RELEASE_NAME") + holmes_enabled = os.environ.get("HOLMES_ENABLED", "false").lower() == "true" + holmes_standalone = os.environ.get("HOLMES_STANDALONE", "false").lower() == "true" + + # Check for explicit Holmes standalone marker + if holmes_standalone: + return DeploymentContext( + method=DeploymentMethod.HELM, + chart_type=ChartType.HOLMES, + release_name=release_name, + namespace=installation_namespace, + ) + + # Robusta Helm deployment: has playbooks config path pointing to /etc/robusta/ + if playbooks_config_path and playbooks_config_path.startswith("/etc/robusta/"): + return DeploymentContext( + method=DeploymentMethod.HELM, + chart_type=ChartType.ROBUSTA, + release_name=release_name, + namespace=installation_namespace, + ) + + # Check if we're in a Kubernetes environment (has namespace set from pod metadata) + # This would indicate Helm deployment even without the config path + if installation_namespace and installation_namespace != "robusta": + # Non-default namespace suggests actual Helm deployment + chart_type = ChartType.HOLMES if holmes_enabled and not playbooks_config_path else ChartType.ROBUSTA + return DeploymentContext( + method=DeploymentMethod.HELM, + chart_type=chart_type, + release_name=release_name, + namespace=installation_namespace, + ) + + # If we have a release name that's not the default, likely Helm + if release_name and release_name != "robusta": + chart_type = ChartType.HOLMES if holmes_enabled and not playbooks_config_path else ChartType.ROBUSTA + return DeploymentContext( + method=DeploymentMethod.HELM, + chart_type=chart_type, + release_name=release_name, + namespace=installation_namespace, + ) + + # Default to CLI if no Helm indicators found + return DeploymentContext( + method=DeploymentMethod.CLI, + chart_type=ChartType.UNKNOWN, + ) + + +def get_env_var_guidance(var_name: str, context: Optional[DeploymentContext] = None) -> str: + """ + Generate guidance for setting an environment variable based on deployment context. + + Args: + var_name: The name of the environment variable + context: Optional pre-detected deployment context. If None, will be detected. + + Returns: + A helpful message with instructions for the detected deployment method. + """ + if context is None: + context = detect_deployment_context() + + if context.is_cli: + return _get_cli_guidance(var_name) + elif context.is_helm: + if context.is_robusta_chart: + return _get_robusta_helm_guidance(var_name) + elif context.is_holmes_chart: + return _get_holmes_helm_guidance(var_name) + else: + # Generic Helm guidance + return _get_generic_helm_guidance(var_name) + else: + return _get_generic_guidance(var_name) + + +def _get_cli_guidance(var_name: str) -> str: + """Guidance for CLI users.""" + return f"""Environment variable '{var_name}' is not set. + +To fix this issue, set the environment variable before running the command: + + export {var_name}= + +Or pass it inline: + + {var_name}= robusta """ + + +def _get_robusta_helm_guidance(var_name: str) -> str: + """Guidance for Robusta Helm chart users.""" + return f"""Environment variable '{var_name}' is not set. + +To fix this issue, add the environment variable to your Robusta Helm values.yaml: + + runner: + additional_env_vars: + - name: {var_name} + value: "" + + # Or use a Kubernetes secret: + runner: + additional_env_vars: + - name: {var_name} + valueFrom: + secretKeyRef: + name: + key: + +Then upgrade your Helm release: + + helm upgrade robusta robusta/robusta -f values.yaml + +For more information, see: https://docs.robusta.dev/master/configuration/index.html""" + + +def _get_holmes_helm_guidance(var_name: str) -> str: + """Guidance for Holmes standalone Helm chart users.""" + return f"""Environment variable '{var_name}' is not set. + +To fix this issue, add the environment variable to your Holmes Helm values.yaml: + + additionalEnvVars: + - name: {var_name} + value: "" + + # Or use a Kubernetes secret: + additionalEnvVars: + - name: {var_name} + valueFrom: + secretKeyRef: + name: + key: + +Then upgrade your Helm release: + + helm upgrade holmes robusta/holmes -f values.yaml + +For more information, see: https://holmesgpt.dev/getting-started/""" + + +def _get_generic_helm_guidance(var_name: str) -> str: + """Generic Helm guidance when chart type is unknown.""" + return f"""Environment variable '{var_name}' is not set. + +To fix this issue, add the environment variable to your Helm values.yaml: + + additionalEnvVars: + - name: {var_name} + value: "" + + # Or use a Kubernetes secret: + additionalEnvVars: + - name: {var_name} + valueFrom: + secretKeyRef: + name: + key: + +Then upgrade your Helm release with: + + helm upgrade -f values.yaml""" + + +def _get_generic_guidance(var_name: str) -> str: + """Generic guidance when deployment method is unknown.""" + return f"""Environment variable '{var_name}' is not set. + +To fix this issue: + + For CLI users: + export {var_name}= + + For Helm chart users (Holmes or Robusta): + Add the environment variable to your values.yaml: + + additionalEnvVars: + - name: {var_name} + value: "" + + # Or use a secret: + additionalEnvVars: + - name: {var_name} + valueFrom: + secretKeyRef: + name: + key: """ diff --git a/tests/test_deployment_context.py b/tests/test_deployment_context.py new file mode 100644 index 000000000..5165ecf0c --- /dev/null +++ b/tests/test_deployment_context.py @@ -0,0 +1,175 @@ +"""Tests for deployment context detection utility.""" + +import os +from unittest import mock + +import pytest + +from robusta.core.model.deployment_context import ( + ChartType, + DeploymentContext, + DeploymentMethod, + detect_deployment_context, + get_env_var_guidance, +) + + +class TestDeploymentContext: + """Tests for the DeploymentContext class.""" + + def test_cli_context_properties(self): + context = DeploymentContext( + method=DeploymentMethod.CLI, + chart_type=ChartType.UNKNOWN, + ) + assert context.is_cli is True + assert context.is_helm is False + assert context.is_robusta_chart is False + assert context.is_holmes_chart is False + + def test_helm_robusta_context_properties(self): + context = DeploymentContext( + method=DeploymentMethod.HELM, + chart_type=ChartType.ROBUSTA, + release_name="my-robusta", + namespace="monitoring", + ) + assert context.is_cli is False + assert context.is_helm is True + assert context.is_robusta_chart is True + assert context.is_holmes_chart is False + assert context.release_name == "my-robusta" + assert context.namespace == "monitoring" + + def test_helm_holmes_context_properties(self): + context = DeploymentContext( + method=DeploymentMethod.HELM, + chart_type=ChartType.HOLMES, + release_name="holmes", + namespace="default", + ) + assert context.is_cli is False + assert context.is_helm is True + assert context.is_robusta_chart is False + assert context.is_holmes_chart is True + + +class TestDetectDeploymentContext: + """Tests for detect_deployment_context function.""" + + def test_detects_cli_with_no_env_vars(self): + with mock.patch.dict(os.environ, {}, clear=True): + context = detect_deployment_context() + assert context.method == DeploymentMethod.CLI + assert context.chart_type == ChartType.UNKNOWN + + def test_detects_cli_with_default_values(self): + env = { + "INSTALLATION_NAMESPACE": "robusta", + "RELEASE_NAME": "robusta", + } + with mock.patch.dict(os.environ, env, clear=True): + context = detect_deployment_context() + assert context.method == DeploymentMethod.CLI + + def test_detects_robusta_helm_with_playbooks_config(self): + env = { + "PLAYBOOKS_CONFIG_FILE_PATH": "/etc/robusta/config/active_playbooks.yaml", + "INSTALLATION_NAMESPACE": "monitoring", + "RELEASE_NAME": "my-robusta", + } + with mock.patch.dict(os.environ, env, clear=True): + context = detect_deployment_context() + assert context.method == DeploymentMethod.HELM + assert context.chart_type == ChartType.ROBUSTA + assert context.release_name == "my-robusta" + assert context.namespace == "monitoring" + + def test_detects_helm_with_non_default_namespace(self): + env = { + "INSTALLATION_NAMESPACE": "custom-namespace", + } + with mock.patch.dict(os.environ, env, clear=True): + context = detect_deployment_context() + assert context.method == DeploymentMethod.HELM + + def test_detects_helm_with_non_default_release_name(self): + env = { + "RELEASE_NAME": "custom-release", + } + with mock.patch.dict(os.environ, env, clear=True): + context = detect_deployment_context() + assert context.method == DeploymentMethod.HELM + + def test_detects_holmes_standalone_with_explicit_marker(self): + env = { + "HOLMES_STANDALONE": "true", + "INSTALLATION_NAMESPACE": "holmes", + "RELEASE_NAME": "holmes", + } + with mock.patch.dict(os.environ, env, clear=True): + context = detect_deployment_context() + assert context.method == DeploymentMethod.HELM + assert context.chart_type == ChartType.HOLMES + + def test_detects_holmes_with_enabled_flag_no_playbooks(self): + env = { + "HOLMES_ENABLED": "true", + "INSTALLATION_NAMESPACE": "custom-ns", + } + with mock.patch.dict(os.environ, env, clear=True): + context = detect_deployment_context() + assert context.method == DeploymentMethod.HELM + assert context.chart_type == ChartType.HOLMES + + +class TestGetEnvVarGuidance: + """Tests for get_env_var_guidance function.""" + + def test_cli_guidance(self): + context = DeploymentContext( + method=DeploymentMethod.CLI, + chart_type=ChartType.UNKNOWN, + ) + guidance = get_env_var_guidance("MY_VAR", context) + assert "export MY_VAR=" in guidance + assert "MY_VAR= robusta" in guidance + assert "Helm" not in guidance + + def test_robusta_helm_guidance(self): + context = DeploymentContext( + method=DeploymentMethod.HELM, + chart_type=ChartType.ROBUSTA, + ) + guidance = get_env_var_guidance("MY_VAR", context) + assert "runner:" in guidance + assert "additional_env_vars:" in guidance + assert "helm upgrade robusta robusta/robusta" in guidance + assert "docs.robusta.dev" in guidance + + def test_holmes_helm_guidance(self): + context = DeploymentContext( + method=DeploymentMethod.HELM, + chart_type=ChartType.HOLMES, + ) + guidance = get_env_var_guidance("MY_VAR", context) + assert "additionalEnvVars:" in guidance + assert "helm upgrade holmes robusta/holmes" in guidance + assert "holmesgpt.dev" in guidance + + def test_guidance_with_auto_detection(self): + # When no context is provided, it should auto-detect + with mock.patch.dict(os.environ, {}, clear=True): + guidance = get_env_var_guidance("TEST_VAR") + assert "TEST_VAR" in guidance + # Should be CLI guidance since no env vars set + assert "export TEST_VAR=" in guidance + + def test_generic_helm_guidance_unknown_chart(self): + context = DeploymentContext( + method=DeploymentMethod.HELM, + chart_type=ChartType.UNKNOWN, + ) + guidance = get_env_var_guidance("MY_VAR", context) + assert "additionalEnvVars:" in guidance + assert "" in guidance