diff --git a/docs/ADRs/006-helm-style-logging.md b/docs/ADRs/006-helm-style-logging.md index fb4a920..d72a027 100644 --- a/docs/ADRs/006-helm-style-logging.md +++ b/docs/ADRs/006-helm-style-logging.md @@ -31,6 +31,12 @@ class HelmLogger: if args: msg = msg % args print("Error: %s" % msg, file=sys.stderr) + + @staticmethod + def warning(msg: str, *args: Any) -> None: + if args: + msg = msg % args + print("Warning: %s" % msg, file=sys.stderr) ``` ### 2. Key Design Decisions @@ -39,6 +45,7 @@ class HelmLogger: - Use stderr for all output - Prefix debug messages with "[debug]" - Prefix error messages with "Error:" + - Prefix warning messages with "Warning:" - Control debug output via HELM_DEBUG environment variable 2. **Performance Optimization** @@ -67,6 +74,7 @@ def some_function(): logger.debug("Success!") except Exception as e: logger.error("Failed: %s", str(e)) + logger.warning("Something unexpected happened") ``` ## Consequences @@ -95,7 +103,7 @@ def some_function(): ### Negative 1. **Limited Flexibility** - Fixed output format - - No log levels beyond debug/error + - Limited log levels (debug/warning/error) - No log file support 2. **Global State** @@ -113,6 +121,12 @@ def test_debug_output(): mock.patch('helm_values_manager.utils.logger.sys.stderr', stderr): logger.debug("Test message") assert stderr.getvalue() == "[debug] Test message\n" + +def test_warning_output(): + stderr = StringIO() + with mock.patch('helm_values_manager.utils.logger.sys.stderr', stderr): + logger.warning("Test warning") + assert stderr.getvalue() == "Warning: Test warning\n" ``` 2. **Integration** @@ -122,4 +136,5 @@ def validate(self): logger.debug("Validating PathData for path: %s", self.path) if not self.is_valid(): logger.error("Invalid PathData: %s", self.path) + logger.warning("PathData validation completed with warnings") ``` diff --git a/docs/ADRs/011-command-structure-for-deployments.md b/docs/ADRs/011-command-structure-for-deployments.md new file mode 100644 index 0000000..6b7b352 --- /dev/null +++ b/docs/ADRs/011-command-structure-for-deployments.md @@ -0,0 +1,83 @@ +# ADR-011: Command Structure for Deployments and Backends + +Date: 2025-02-28 + +## Status + +Accepted + +## Context + +The Helm Values Manager needs to support multiple backend types (git-secret, aws, azure, gcp) and authentication methods (env, file, direct, managed_identity) for storing and retrieving values. The current implementation uses a single `add-deployment` command with many parameters, which can be complex and difficult to use. + +As we implement more backend types and authentication methods, the command structure needs to be intuitive, discoverable, and maintainable. The command interface should guide users to provide the correct parameters for each backend and authentication type. + +## Decision + +We will adopt a nested subcommand structure for deployment and backend management with the following pattern: + +``` +helm values add-deployment [name] + +helm values add-backend [backend] --deployment=[name] [backend_options] + - helm values add-backend aws --deployment=prod --region=us-west-2 + - helm values add-backend azure --deployment=prod --vault-url=https://myvault.vault.azure.net + - helm values add-backend gcp --deployment=prod --project-id=my-project + - helm values add-backend git-secret --deployment=prod +``` + +Initially, deployments will be created with a `no-backend` type and `no-auth` authentication, indicating that they don't have a backend or authentication configured yet. This allows for deployments that might not need sensitive values, while providing a clear path to add a backend later if needed. + +For authentication, we will use a similar pattern: + +``` +helm values add-auth [auth_type] --deployment=[name] [auth_options] + - helm values add-auth direct --deployment=prod --credentials='{...}' + - helm values add-auth env --deployment=prod --env-prefix=AWS_ + - helm values add-auth file --deployment=prod --auth-path=~/.aws/credentials + - helm values add-auth managed-identity --deployment=prod +``` + +This approach: +1. Separates the concerns of creating a deployment, configuring a backend, and setting up authentication +2. Uses subcommands to provide context-specific options and help text +3. Follows a natural workflow of first creating a deployment, then adding backend and auth configuration +4. Supports deployments without sensitive values through the `no-backend` option + +For the initial implementation, we will only implement the `add-deployment` command, with the backend and auth commands to be implemented later. + +## Consequences + +### Positive + +- **Improved User Experience**: Users only see options relevant to their current task +- **Better Discoverability**: Each subcommand can have its own help text explaining the specific options +- **Progressive Disclosure**: Complex options are only shown when needed +- **Reduced Cognitive Load**: Users don't need to remember all possible combinations of options +- **Better Validation**: Each command can validate its specific inputs more effectively +- **Maintainability**: New backend types and auth methods can be added without changing existing commands +- **Flexibility**: Users can create deployments without immediately configuring backends, allowing for more flexible workflows +- **Safety**: The `no-backend` option provides a clear indication that a deployment is not configured for sensitive values, and validation can prevent adding sensitive values to such deployments + +### Negative + +- **More Commands**: Users need to run multiple commands to fully configure a deployment +- **Learning Curve**: Users need to learn the command structure and workflow +- **Implementation Complexity**: More commands means more code to maintain + +### Neutral + +- **Alignment with Other Tools**: This approach is similar to other CLI tools like `git` and `kubectl` +- **Documentation Requirements**: We will need to document the command workflow clearly + +## Implementation Notes + +1. For the initial implementation, we will only implement the `add-deployment` command, which will create a deployment with minimal configuration. +2. The `add-deployment` command will validate that the deployment name is unique and create a basic deployment entry in the configuration. +3. The backend and auth commands will be implemented later, with appropriate validation to ensure that the referenced deployment exists. +4. We will use Typer's nested command structure to implement this design. +5. The `Deployment` class will need to support a "partial" state where backend and auth configuration may not be fully defined yet. + +## Related Issues + +- GitHub Issue #[TBD]: Implement command structure for deployments and backends diff --git a/docs/ADRs/README.md b/docs/ADRs/README.md index 11f3fd5..3d937ea 100644 --- a/docs/ADRs/README.md +++ b/docs/ADRs/README.md @@ -78,3 +78,10 @@ For new ADRs, use the [ADR template](adr-template.md) as a starting point. - **Decision**: Implement configuration comparison and smart merging with multiple strategies - **Impact**: Simplifies configuration updates and reduces risk of missing required changes - **Dependencies**: ADR-001 + +### [ADR-011: Command Structure for Deployments and Backends](011-command-structure-for-deployments.md) +- **Status**: Accepted +- **Context**: Need for intuitive command structure for managing deployments with multiple backends and auth types +- **Decision**: Implement nested subcommand structure for deployment, backend, and auth configuration +- **Impact**: Improves user experience, discoverability, and maintainability +- **Dependencies**: ADR-001 diff --git a/docs/Design/backends-and-auth.md b/docs/Design/backends-and-auth.md new file mode 100644 index 0000000..72a1b66 --- /dev/null +++ b/docs/Design/backends-and-auth.md @@ -0,0 +1,188 @@ +# Backends and Authentication Types + +This document provides a comprehensive overview of the supported backends, authentication types, and backend configurations in the Helm Values Manager. + +## Supported Backends + +The Helm Values Manager supports the following backend types for storing values: + +| Backend Type | Description | Use Case | Status | +|--------------|-------------|----------|--------| +| `git-secret` | Uses git-secret for encrypting sensitive values | Local development, small teams | Planned | +| `aws` | Uses AWS Secrets Manager for storing sensitive values | AWS-based deployments | Planned | +| `azure` | Uses Azure Key Vault for storing sensitive values | Azure-based deployments | Planned | +| `gcp` | Uses Google Secret Manager for storing sensitive values | GCP-based deployments | Planned | + +### Backend Selection Criteria + +When selecting a backend, consider: + +1. **Security Requirements**: Different backends offer varying levels of security, audit capabilities, and compliance features. +2. **Cloud Provider**: Select the backend that aligns with your cloud infrastructure. +3. **Team Size**: For small teams, simpler backends like `git-secret` may be sufficient. +4. **Operational Complexity**: Some backends require more setup and maintenance than others. + +## Authentication Types + +Each backend supports multiple authentication methods: + +| Auth Type | Description | Required Parameters | Supported Backends | +|-----------|-------------|---------------------|-------------------| +| `direct` | Direct credential input | `credentials` object | All | +| `env` | Environment variable-based authentication | `env_prefix` | All | +| `file` | File-based authentication | `path` to auth file | All | +| `managed_identity` | Cloud provider managed identity | None | `aws`, `azure`, `gcp` | + +### Authentication Type Details + +#### Direct Authentication (`direct`) + +Credentials are provided directly in the configuration file. This is suitable for testing but not recommended for production use. + +**Required Parameters:** +- `credentials`: An object containing backend-specific credentials + +**Example:** +```json +"auth": { + "type": "direct", + "credentials": { + "token": "your-token-here" + } +} +``` + +#### Environment Variable Authentication (`env`) + +Credentials are read from environment variables. This is suitable for CI/CD pipelines and containerized deployments. + +**Required Parameters:** +- `env_prefix`: Prefix for environment variables + +**Example:** +```json +"auth": { + "type": "env", + "env_prefix": "AWS_" +} +``` + +#### File Authentication (`file`) + +Credentials are read from a file. This is suitable for local development and when credentials are managed by external systems. + +**Required Parameters:** +- `path`: Path to the authentication file + +**Example:** +```json +"auth": { + "type": "file", + "path": "~/.aws/credentials" +} +``` + +#### Managed Identity Authentication (`managed_identity`) + +Uses cloud provider's managed identity service. This is the recommended approach for production deployments in cloud environments. + +**Required Parameters:** +- None + +**Example:** +```json +"auth": { + "type": "managed_identity" +} +``` + +## Backend Configurations + +Each backend may require additional configuration parameters: + +### Git Secret Backend (`git-secret`) + +| Parameter | Description | Required | Default | +|-----------|-------------|----------|---------| +| None | No additional configuration required | - | - | + +### AWS Secrets Manager Backend (`aws`) + +| Parameter | Description | Required | Default | +|-----------|-------------|----------|---------| +| `region` | AWS region | Yes | - | +| `prefix` | Prefix for secret names | No | Empty string | +| `endpoint` | Custom endpoint URL | No | AWS default endpoint | + +**Example:** +```json +"backend_config": { + "region": "us-west-2", + "prefix": "myapp/" +} +``` + +### Azure Key Vault Backend (`azure`) + +| Parameter | Description | Required | Default | +|-----------|-------------|----------|---------| +| `vault_url` | Key Vault URL | Yes | - | +| `prefix` | Prefix for secret names | No | Empty string | + +**Example:** +```json +"backend_config": { + "vault_url": "https://myvault.vault.azure.net/", + "prefix": "myapp-" +} +``` + +### Google Secret Manager Backend (`gcp`) + +| Parameter | Description | Required | Default | +|-----------|-------------|----------|---------| +| `project_id` | GCP Project ID | Yes | - | +| `prefix` | Prefix for secret names | No | Empty string | + +**Example:** +```json +"backend_config": { + "project_id": "my-gcp-project", + "prefix": "myapp_" +} +``` + +## Implementation Status + +For the MVP release, the following components are implemented: + +1. **Command Interface**: + - `add-deployment`: Command interface implemented + - Backend validation: Interface defined, implementation pending + +2. **Backends**: + - All backends: Interface defined, implementation pending + +3. **Authentication Types**: + - All auth types: Interface defined, implementation pending + +4. **Backend Configurations**: + - Basic validation implemented + - Backend-specific validation defined in the command interface + +## Future Enhancements + +1. **Additional Backends**: + - HashiCorp Vault + - Kubernetes Secrets + - Custom backends via plugins + +2. **Enhanced Authentication**: + - OIDC support + - Role-based access for cloud providers + - Multi-factor authentication integration + +3. **Configuration Extensions**: + - Rotation policies + - Versioning support + - Audit logging diff --git a/docs/Design/low-level-design.md b/docs/Design/low-level-design.md index 0354430..1cd31ce 100644 --- a/docs/Design/low-level-design.md +++ b/docs/Design/low-level-design.md @@ -174,6 +174,31 @@ Benefits: - Automatic file locking - Configuration backup support +#### Command Structure for Deployments and Backends + +The command structure for managing deployments and backends follows a nested subcommand pattern (see [ADR-011](../ADRs/011-command-structure-for-deployments.md)): + +``` +helm values add-deployment [name] + +helm values add-backend [backend] --deployment=[name] [backend_options] + - helm values add-backend aws --deployment=prod --region=us-west-2 + - helm values add-backend azure --deployment=prod --vault-url=https://myvault.vault.azure.net + - helm values add-backend gcp --deployment=prod --project-id=my-project + - helm values add-backend git-secret --deployment=prod + +helm values add-auth [auth_type] --deployment=[name] [auth_options] + - helm values add-auth direct --deployment=prod --credentials='{...}' + - helm values add-auth env --deployment=prod --env-prefix=AWS_ + - helm values add-auth file --deployment=prod --auth-path=~/.aws/credentials + - helm values add-auth managed-identity --deployment=prod +``` + +This structure: +- Separates the concerns of creating a deployment, configuring a backend, and setting up authentication +- Uses subcommands to provide context-specific options and help text +- Follows a natural workflow of first creating a deployment, then adding backend and auth configuration + ### 4. Storage Backends The `ValueBackend` interface defines the contract for value storage: @@ -202,6 +227,8 @@ Implementations: - Azure Key Vault Backend - Additional backends can be easily added +For a comprehensive overview of supported backends, authentication types, and backend configurations, see [Backends and Authentication Types](backends-and-auth.md). + ### 5. Schema Validation The configuration system uses JSON Schema validation to ensure data integrity and consistency: diff --git a/docs/Design/sequence-diagrams.md b/docs/Design/sequence-diagrams.md index ea700c0..1148176 100644 --- a/docs/Design/sequence-diagrams.md +++ b/docs/Design/sequence-diagrams.md @@ -83,6 +83,44 @@ sequenceDiagram ## 3. Add Deployment Command Flow +```mermaid +sequenceDiagram + participant User + participant CLI + participant BaseCommand + participant HelmValuesConfig + + User->>CLI: helm values add-deployment prod + activate CLI + + CLI->>BaseCommand: execute() + activate BaseCommand + + BaseCommand->>BaseCommand: acquire_lock() + BaseCommand->>BaseCommand: load_config() + BaseCommand->>HelmValuesConfig: add_deployment(name) + activate HelmValuesConfig + + HelmValuesConfig->>HelmValuesConfig: validate_deployment_name(name) + HelmValuesConfig->>HelmValuesConfig: create_deployment(name) + HelmValuesConfig-->>BaseCommand: success + deactivate HelmValuesConfig + + BaseCommand->>FileSystem: write_config_file() + activate FileSystem + FileSystem-->>BaseCommand: success + deactivate FileSystem + + BaseCommand->>BaseCommand: release_lock() + BaseCommand-->>CLI: success + deactivate BaseCommand + + CLI-->>User: "Added deployment 'prod'" + deactivate CLI +``` + +## 3.1 Add Backend Command Flow (Future Implementation) + ```mermaid sequenceDiagram participant User @@ -91,7 +129,7 @@ sequenceDiagram participant HelmValuesConfig participant ValueBackend - User->>CLI: helm values add-deployment --name=prod --backend=aws + User->>CLI: helm values add-backend aws --deployment=prod --region=us-west-2 activate CLI CLI->>BaseCommand: execute() @@ -99,15 +137,16 @@ sequenceDiagram BaseCommand->>BaseCommand: acquire_lock() BaseCommand->>BaseCommand: load_config() - BaseCommand->>HelmValuesConfig: add_deployment(name, backend, auth) + BaseCommand->>HelmValuesConfig: add_backend_to_deployment(name, backend, backend_config) activate HelmValuesConfig - HelmValuesConfig->>ValueBackend: validate_auth_config(auth) + HelmValuesConfig->>HelmValuesConfig: validate_deployment_exists(name) + HelmValuesConfig->>ValueBackend: validate_backend_config(backend, backend_config) activate ValueBackend ValueBackend-->>HelmValuesConfig: validation result deactivate ValueBackend - HelmValuesConfig->>HelmValuesConfig: update_deployments() + HelmValuesConfig->>HelmValuesConfig: update_deployment_backend(name, backend, backend_config) HelmValuesConfig-->>BaseCommand: success deactivate HelmValuesConfig @@ -120,7 +159,51 @@ sequenceDiagram BaseCommand-->>CLI: success deactivate BaseCommand - CLI-->>User: "Added deployment 'prod'" + CLI-->>User: "Added aws backend to deployment 'prod'" + deactivate CLI +``` + +## 3.2 Add Auth Command Flow (Future Implementation) + +```mermaid +sequenceDiagram + participant User + participant CLI + participant BaseCommand + participant HelmValuesConfig + participant ValueBackend + + User->>CLI: helm values add-auth direct --deployment=prod --credentials='{...}' + activate CLI + + CLI->>BaseCommand: execute() + activate BaseCommand + + BaseCommand->>BaseCommand: acquire_lock() + BaseCommand->>BaseCommand: load_config() + BaseCommand->>HelmValuesConfig: add_auth_to_deployment(name, auth_type, auth_config) + activate HelmValuesConfig + + HelmValuesConfig->>HelmValuesConfig: validate_deployment_exists(name) + HelmValuesConfig->>ValueBackend: validate_auth_config(auth_type, auth_config) + activate ValueBackend + ValueBackend-->>HelmValuesConfig: validation result + deactivate ValueBackend + + HelmValuesConfig->>HelmValuesConfig: update_deployment_auth(name, auth_type, auth_config) + HelmValuesConfig-->>BaseCommand: success + deactivate HelmValuesConfig + + BaseCommand->>FileSystem: write_config_file() + activate FileSystem + FileSystem-->>BaseCommand: success + deactivate FileSystem + + BaseCommand->>BaseCommand: release_lock() + BaseCommand-->>CLI: success + deactivate BaseCommand + + CLI-->>User: "Added direct authentication to deployment 'prod'" deactivate CLI ``` diff --git a/docs/Development/tasks.md b/docs/Development/tasks.md index c5b5cff..5aaa0b8 100644 --- a/docs/Development/tasks.md +++ b/docs/Development/tasks.md @@ -179,6 +179,7 @@ - [x] Implement Helm-style logger - [x] Create HelmLogger class - [x] Add debug and error methods + - [x] Add warning method - [x] Follow Helm output conventions - [x] Add HELM_DEBUG support - [x] Add comprehensive tests @@ -226,6 +227,24 @@ - [ ] Add atomic writes - [ ] Optimize backup strategy +## Deployment and Backend Management +- [x] Implement new command structure (ADR-011) + - [x] Create ADR for command structure + - [x] Implement add-deployment command + - [x] Update deployment model to support partial configurations + - [x] Add tests for add-deployment command + - [x] Add support for NO_BACKEND and NO_AUTH options + - [x] Add unit and integration tests for CLI add-deployment command + - [ ] Implement add-backend commands (future) + - [ ] Implement add-auth commands (future) + - [x] Update documentation with new command structure +- [ ] Implement value management commands + - [ ] Implement add-value command + - [ ] Add validation to prevent adding sensitive values to NO_BACKEND deployments + - [ ] Implement get-value command + - [ ] Implement update-value command + - [ ] Implement delete-value command + ## Clean Up Tasks - [x] Remove deprecated code - [x] Clean up old backend implementations (removed PlainTextBackend) diff --git a/helm_values_manager/cli.py b/helm_values_manager/cli.py index b0da144..3388a00 100644 --- a/helm_values_manager/cli.py +++ b/helm_values_manager/cli.py @@ -4,6 +4,7 @@ import typer +from helm_values_manager.commands.add_deployment_command import AddDeploymentCommand from helm_values_manager.commands.add_value_config_command import AddValueConfigCommand from helm_values_manager.commands.init_command import InitCommand from helm_values_manager.utils.logger import HelmLogger @@ -76,5 +77,25 @@ def add_value_config( raise typer.Exit(code=1) from e +@app.command("add-deployment") +def add_deployment( + name: str = typer.Argument(..., help="Deployment name (e.g., 'dev', 'prod')"), +): + """Add a new deployment configuration.""" + try: + command = AddDeploymentCommand() + + # Create kwargs for command execution + kwargs = { + "name": name, + } + + result = command.execute(**kwargs) + typer.echo(result) + except Exception as e: + HelmLogger.error("Failed to add deployment: %s", str(e)) + raise typer.Exit(code=1) from e + + if __name__ == "__main__": app(prog_name=COMMAND_INFO) diff --git a/helm_values_manager/commands/add_deployment_command.py b/helm_values_manager/commands/add_deployment_command.py new file mode 100644 index 0000000..9d01836 --- /dev/null +++ b/helm_values_manager/commands/add_deployment_command.py @@ -0,0 +1,57 @@ +"""Command to add a new deployment configuration.""" + +from typing import Optional + +from helm_values_manager.commands.base_command import BaseCommand +from helm_values_manager.models.constants import NO_AUTH, NO_BACKEND +from helm_values_manager.models.helm_values_config import Deployment, HelmValuesConfig +from helm_values_manager.utils.logger import HelmLogger + + +class AddDeploymentCommand(BaseCommand): + """Command to add a new deployment configuration.""" + + def run(self, config: Optional[HelmValuesConfig] = None, **kwargs) -> str: + """ + Add a new deployment configuration. + + Args: + config: The loaded configuration + **kwargs: Command arguments + - name (str): The deployment name (e.g., 'dev', 'prod') + + Returns: + str: Success message + + Raises: + ValueError: If required parameters are missing or invalid + """ + if config is None: + raise ValueError("Configuration not loaded") + + # Extract required parameters + name = kwargs.get("name") + if not name: + raise ValueError("Deployment name cannot be empty") + + # Check if deployment already exists + if name in config.deployments: + raise ValueError(f"Deployment '{name}' already exists") + + # Create and add the deployment with minimal configuration + # Backend and auth will be added later with separate commands + deployment = Deployment( + name=name, + backend=NO_BACKEND, # Default to NO_BACKEND, will be updated by add-backend command + auth={"type": NO_AUTH}, # Default to NO_AUTH, will be updated by add-auth command + backend_config={}, # Empty dict instead of None + ) + + # Add deployment to config + config.deployments[name] = deployment + + # Save the updated configuration + self.save_config(config) + + HelmLogger.debug("Added deployment '%s'", name) + return f"Successfully added deployment '{name}'" diff --git a/helm_values_manager/models/constants.py b/helm_values_manager/models/constants.py new file mode 100644 index 0000000..e82dd68 --- /dev/null +++ b/helm_values_manager/models/constants.py @@ -0,0 +1,15 @@ +"""Constants used throughout the application.""" + +# Backend types +NO_BACKEND = "no-backend" +GIT_SECRET_BACKEND = "git-secret" +AWS_BACKEND = "aws" +AZURE_BACKEND = "azure" +GCP_BACKEND = "gcp" + +# Auth types +NO_AUTH = "no-auth" +ENV_AUTH = "env" +FILE_AUTH = "file" +DIRECT_AUTH = "direct" +MANAGED_IDENTITY_AUTH = "managed_identity" diff --git a/helm_values_manager/schemas/v1.json b/helm_values_manager/schemas/v1.json index 87a69e8..28a4f33 100644 --- a/helm_values_manager/schemas/v1.json +++ b/helm_values_manager/schemas/v1.json @@ -21,7 +21,7 @@ "properties": { "backend": { "type": "string", - "enum": ["git-secret", "aws", "azure", "gcp"], + "enum": ["no-backend", "git-secret", "aws", "azure", "gcp"], "description": "Type of backend to use for value storage" }, "auth": { @@ -30,7 +30,7 @@ "properties": { "type": { "type": "string", - "enum": ["env", "file", "direct", "managed_identity"], + "enum": ["no-auth", "env", "file", "direct", "managed_identity"], "description": "Authentication method to use" } }, diff --git a/helm_values_manager/utils/logger.py b/helm_values_manager/utils/logger.py index db2a658..0c921e0 100644 --- a/helm_values_manager/utils/logger.py +++ b/helm_values_manager/utils/logger.py @@ -47,3 +47,16 @@ def error(msg: str, *args: Any) -> None: if args: msg = msg % args print("Error: %s" % msg, file=sys.stderr) + + @staticmethod + def warning(msg: str, *args: Any) -> None: + """ + Print warning message to stderr. + + Args: + msg: Message with optional string format placeholders + args: Values to substitute in the message + """ + if args: + msg = msg % args + print("Warning: %s" % msg, file=sys.stderr) diff --git a/tests/integration/test_cli_integration.py b/tests/integration/test_cli_integration.py index a4b2c41..377cfd9 100644 --- a/tests/integration/test_cli_integration.py +++ b/tests/integration/test_cli_integration.py @@ -192,3 +192,72 @@ def test_add_value_config_duplicate_path(plugin_install, tmp_path): assert returncode != 0 assert f"Path {path} already exists" in stderr + + +def test_add_deployment_help_command(plugin_install): + """Test that the add-deployment help command works and shows expected output.""" + stdout, stderr, returncode = run_helm_command(["values-manager", "add-deployment", "--help"]) + assert returncode == 0 + assert "Add a new deployment configuration" in stdout + + +def test_add_deployment_command(plugin_install, tmp_path): + """Test that the add-deployment command works correctly.""" + # Create a working directory + work_dir = tmp_path / "test_add_deployment" + work_dir.mkdir() + os.chdir(work_dir) + + # Initialize the config + init_stdout, init_stderr, init_returncode = run_helm_command( + ["values-manager", "init", "--release", "test-release"] + ) + assert init_returncode == 0 + assert Path("helm-values.json").exists() + + # Add a deployment + stdout, stderr, returncode = run_helm_command(["values-manager", "add-deployment", "dev"]) + assert returncode == 0 + assert "Successfully added deployment 'dev'" in stdout + + # Verify the deployment was added + with open("helm-values.json", "r") as f: + config = json.load(f) + assert "dev" in config["deployments"] + + +def test_add_deployment_duplicate(plugin_install, tmp_path): + """Test that adding a duplicate deployment fails with the correct error message.""" + # Create a working directory + work_dir = tmp_path / "test_add_deployment_duplicate" + work_dir.mkdir() + os.chdir(work_dir) + + # Initialize the config + init_stdout, init_stderr, init_returncode = run_helm_command( + ["values-manager", "init", "--release", "test-release"] + ) + assert init_returncode == 0 + assert Path("helm-values.json").exists() + + # Add a deployment + stdout, stderr, returncode = run_helm_command(["values-manager", "add-deployment", "dev"]) + assert returncode == 0 + + # Try to add the same deployment again + stdout, stderr, returncode = run_helm_command(["values-manager", "add-deployment", "dev"]) + assert returncode == 1 + assert "Deployment 'dev' already exists" in stderr + + +def test_add_deployment_no_config(plugin_install, tmp_path): + """Test that adding a deployment without initializing fails with the correct error message.""" + # Create a working directory + work_dir = tmp_path / "test_add_deployment_no_config" + work_dir.mkdir() + os.chdir(work_dir) + + # Try to add a deployment without initializing + stdout, stderr, returncode = run_helm_command(["values-manager", "add-deployment", "dev"]) + assert returncode == 1 + assert "Configuration file helm-values.json not found" in stderr diff --git a/tests/unit/commands/test_add_deployment_command.py b/tests/unit/commands/test_add_deployment_command.py new file mode 100644 index 0000000..1e38e32 --- /dev/null +++ b/tests/unit/commands/test_add_deployment_command.py @@ -0,0 +1,86 @@ +"""Unit tests for the add-deployment command.""" + +import json +import os + +import pytest + +from helm_values_manager.commands.add_deployment_command import AddDeploymentCommand +from helm_values_manager.models.constants import NO_AUTH, NO_BACKEND + + +@pytest.fixture +def mock_config_file(tmp_path): + """Create a mock config file for testing.""" + config_path = tmp_path / "helm-values.json" + config_data = {"version": "1.0", "release": "test-release", "deployments": {}, "config": []} + config_path.write_text(json.dumps(config_data)) + return str(config_path) + + +@pytest.fixture +def command(mock_config_file): + """Create a command instance with a mock config file.""" + cmd = AddDeploymentCommand() + cmd.config_file = mock_config_file + cmd.lock_file = str(os.path.dirname(mock_config_file)) + "/.helm-values.lock" + return cmd + + +def test_add_deployment_basic(command): + """Test adding a basic deployment configuration.""" + # Arrange + kwargs = { + "name": "dev", + } + + # Act + result = command.execute(**kwargs) + + # Assert + assert "Successfully added deployment 'dev'" in result + + # Verify the config file was updated correctly + with open(command.config_file, "r") as f: + config_data = json.load(f) + + assert "dev" in config_data["deployments"] + assert config_data["deployments"]["dev"]["backend"] == NO_BACKEND + assert config_data["deployments"]["dev"]["auth"]["type"] == NO_AUTH + assert config_data["deployments"]["dev"]["backend_config"] == {} + + +def test_add_deployment_missing_name(command): + """Test that an error is raised when name is missing.""" + # Arrange + kwargs = {} + + # Act & Assert + with pytest.raises(ValueError, match="Deployment name cannot be empty"): + command.execute(**kwargs) + + +def test_add_deployment_duplicate(command): + """Test that an error is raised when adding a duplicate deployment.""" + # Arrange + kwargs = { + "name": "dev", + } + + # Add the deployment first + command.execute(**kwargs) + + # Act & Assert - Try to add it again + with pytest.raises(ValueError, match="Deployment 'dev' already exists"): + command.execute(**kwargs) + + +def test_add_deployment_no_config(): + """Test that an error is raised when config is None.""" + # Arrange + command = AddDeploymentCommand() + command.config = None + + # Act & Assert + with pytest.raises(ValueError, match="Configuration not loaded"): + command.run(name="dev") diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 72876d7..fe03bce 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -139,3 +139,87 @@ def test_add_value_config_empty_path(tmp_path): assert result.exit_code == 1 assert "Failed to add value config: Path cannot be empty" in result.stdout + + +def test_add_value_config_with_sensitive_flag(tmp_path): + """Test add-value-config command with sensitive flag.""" + # Change to temp directory + os.chdir(tmp_path) + + # Initialize the config first + runner.invoke(app, ["init", "--release", "test-release"], catch_exceptions=False) + + # Add a value config with sensitive flag + result = runner.invoke( + app, + ["add-value-config", "--path", "test.path", "--description", "Test description", "--required", "--sensitive"], + catch_exceptions=False, + ) + assert result.exit_code == 0 + assert "Successfully added value config" in result.stdout + assert "Sensitive value support will be available in version 0.2.0" in result.stdout + + # Verify the config file was updated correctly + config_file = Path("helm-values.json") + with config_file.open() as file: + config_data = json.load(file) + assert len(config_data["config"]) == 1 + assert config_data["config"][0]["path"] == "test.path" + assert config_data["config"][0]["description"] == "Test description" + assert config_data["config"][0]["required"] is True + assert config_data["config"][0]["sensitive"] is False # Should be False since the flag is ignored + + +def test_add_deployment_command(tmp_path): + """Test add-deployment command.""" + # Change to temp directory + os.chdir(tmp_path) + + # First initialize the config + init_result = runner.invoke(app, ["init", "--release", "test-release"], catch_exceptions=False) + assert init_result.exit_code == 0 + + # Now add a deployment + result = runner.invoke(app, ["add-deployment", "dev"], catch_exceptions=False) + print("Command output:", result.stdout) # Debug output + assert result.exit_code == 0 + assert "Successfully added deployment 'dev'" in result.stdout + + # Verify the deployment was added to the config file + config_file = Path("helm-values.json") + with config_file.open() as file: + config_data = json.load(file) + assert "dev" in config_data["deployments"] + assert config_data["deployments"]["dev"]["backend"] == "no-backend" + assert config_data["deployments"]["dev"]["auth"]["type"] == "no-auth" + assert config_data["deployments"]["dev"]["backend_config"] == {} + + +def test_add_deployment_duplicate(tmp_path): + """Test add-deployment command with duplicate deployment.""" + # Change to temp directory + os.chdir(tmp_path) + + # First initialize the config + init_result = runner.invoke(app, ["init", "--release", "test-release"], catch_exceptions=False) + assert init_result.exit_code == 0 + + # Add a deployment + first_result = runner.invoke(app, ["add-deployment", "dev"], catch_exceptions=False) + assert first_result.exit_code == 0 + + # Try to add the same deployment again + second_result = runner.invoke(app, ["add-deployment", "dev"], catch_exceptions=False) + assert second_result.exit_code == 1 + assert "Failed to add deployment: Deployment 'dev' already exists" in second_result.stdout + + +def test_add_deployment_no_config(tmp_path): + """Test add-deployment command without initializing config first.""" + # Change to temp directory + os.chdir(tmp_path) + + # Try to add a deployment without initializing + result = runner.invoke(app, ["add-deployment", "dev"], catch_exceptions=False) + assert result.exit_code == 1 + assert "Configuration file helm-values.json not found" in result.stdout diff --git a/tests/unit/utils/test_logger.py b/tests/unit/utils/test_logger.py index aa4c38b..33ad67f 100644 --- a/tests/unit/utils/test_logger.py +++ b/tests/unit/utils/test_logger.py @@ -73,3 +73,19 @@ def test_multiple_messages(logger): logger.debug("Debug 2") expected = "[debug] Debug 1\nError: Error 1\n[debug] Debug 2\n" assert stderr.getvalue() == expected + + +def test_warning_output(logger): + """Test warning output.""" + stderr = StringIO() + with mock.patch("helm_values_manager.utils.logger.sys.stderr", stderr): + logger.warning("Warning message") + assert stderr.getvalue() == "Warning: Warning message\n" + + +def test_warning_with_formatting(logger): + """Test warning output with string formatting.""" + stderr = StringIO() + with mock.patch("helm_values_manager.utils.logger.sys.stderr", stderr): + logger.warning("Warning in %s: %s", "function", "details") + assert stderr.getvalue() == "Warning: Warning in function: details\n"