Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,18 @@ helm plugin install https://github.com/zipstack/helm-values-manager
## Quick Start

1. Initialize a new configuration:

```bash
helm values-manager init
```

This creates:

- `values-manager.yaml` configuration file
- `values` directory with environment files (`dev.yaml`, `staging.yaml`, `prod.yaml`)

2. View available commands:

```bash
helm values-manager --help
```
Expand All @@ -52,35 +55,41 @@ helm values-manager --help
### Setup Development Environment

1. Clone the repository:

```bash
git clone https://github.com/zipstack/helm-values-manager
cd helm-values-manager
```

2. Create and activate a virtual environment:

```bash
python -m venv venv
source venv/bin/activate # On Windows: .\venv\Scripts\activate
```

3. Install development dependencies:

```bash
pip install -e ".[dev]"
```

4. Install pre-commit hooks:

```bash
pre-commit install
```

### Running Tests

Run tests with tox (will test against multiple Python versions):

```bash
tox
```

Run tests for a specific Python version:

```bash
tox -e py39 # For Python 3.9
```
Expand All @@ -95,6 +104,7 @@ This project uses several tools to maintain code quality:
- **flake8**: Style guide enforcement

Run all code quality checks manually:

```bash
pre-commit run --all-files
```
Expand Down
82 changes: 82 additions & 0 deletions docs/Design/low-level-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,88 @@ Implementations:
- Azure Key Vault Backend
- Additional backends can be easily added

### 5. Schema Validation

The configuration system uses JSON Schema validation to ensure data integrity and consistency:

```mermaid
classDiagram
class SchemaValidator {
+validate_config(data: dict) None
-load_schema() dict
-handle_validation_error(error: ValidationError) str
}

class HelmValuesConfig {
+from_dict(data: dict) HelmValuesConfig
+to_dict() dict
+validate() None
}

HelmValuesConfig ..> SchemaValidator : uses
```

#### Schema Structure

The schema (`schemas/v1.json`) defines:
1. **Version Control**
- Schema version validation
- Backward compatibility checks

2. **Deployment Configuration**
- Backend type validation (git-secret, aws, azure, gcp)
- Authentication method validation
- Backend-specific configuration validation

3. **Value Configuration**
- Path format validation (dot notation)
- Required/optional field validation
- Sensitive value handling
- Environment-specific value validation

#### Validation Points

Schema validation occurs at critical points:
1. **Configuration Loading** (`from_dict`)
- Validates complete configuration structure
- Ensures all required fields are present
- Validates data types and formats

2. **Pre-save Validation** (`to_dict`)
- Ensures configuration remains valid after modifications
- Validates new values match schema requirements

3. **Path Addition** (`add_config_path`)
- Validates new path format
- Ensures path uniqueness
- Validates metadata structure

#### Error Handling

The validation system provides:
1. **Detailed Error Messages**
- Exact location of validation failures
- Clear explanation of validation rules
- Suggestions for fixing issues

2. **Validation Categories**
- Schema version mismatches
- Missing required fields
- Invalid value formats
- Backend configuration errors
- Authentication configuration errors

3. **Error Recovery**
- Validation before persistence
- Prevents invalid configurations from being saved
- Maintains system consistency

This validation ensures:
- Configuration integrity
- Consistent data structure
- Clear error reporting
- Safe configuration updates

## Implementation Details

### 1. Configuration Structure
Expand Down
30 changes: 14 additions & 16 deletions docs/Development/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@
- [x] Add tests for ConfigMetadata
- [x] Integrate with PathData

### Schema Validation Integration
- [x] Add Basic Schema Validation
- [x] Create test_schema_validation.py
- [x] Test valid configuration loading
- [x] Test invalid configuration detection
- [x] Test error message clarity
- [x] Add schema validation to HelmValuesConfig
- [x] Add jsonschema dependency
- [x] Implement validation in from_dict
- [x] Add clear error messages
- [x] Update documentation
- [x] Schema documentation in low-level design
- [x] Example configuration in design docs

### ConfigMetadata
- [x] Implement ConfigMetadata class
- [x] Add metadata attributes
Expand All @@ -45,21 +59,6 @@
- [x] Implement from_dict() static method
- [x] Add tests for serialization/deserialization

### HelmValuesConfig Refactoring
- [ ] Remove PlainTextBackend references
- [ ] Update imports and dependencies
- [ ] Remove plaintext.py
- [ ] Update tests
- [ ] Implement unified path storage
- [ ] Add _path_map dictionary
- [ ] Migrate existing code to use _path_map
- [ ] Update tests for new structure
- [ ] Update value management
- [ ] Refactor set_value() to use Value class
- [ ] Refactor get_value() to use Value class
- [ ] Add value validation in set operations
- [ ] Update tests for new value handling

### Backend System
- [ ] Clean up base ValueBackend
- [ ] Update interface methods
Expand Down Expand Up @@ -97,7 +96,6 @@
- [x] Value class tests
- [x] PathData class tests
- [x] ConfigMetadata tests
- [ ] HelmValuesConfig tests
- [ ] Backend tests
- [ ] Command tests
- [ ] Add integration tests
Expand Down
183 changes: 183 additions & 0 deletions helm_values_manager/models/helm_values_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
"""HelmValuesConfig class for managing Helm values and secrets."""

import json
import os
from dataclasses import dataclass, field
from typing import Any, Dict, Optional

import jsonschema
from jsonschema.exceptions import ValidationError

from helm_values_manager.backends.simple import SimpleValueBackend
from helm_values_manager.models.path_data import PathData
from helm_values_manager.models.value import Value


@dataclass
class Deployment:
"""Deployment configuration."""

name: str
auth: Dict[str, Any]
backend: str
backend_config: Dict[str, Any] = field(default_factory=dict)

def to_dict(self) -> Dict[str, Any]:
"""Convert deployment to dictionary."""
return {"backend": self.backend, "auth": self.auth, "backend_config": self.backend_config}


class HelmValuesConfig:
"""Configuration manager for Helm values and secrets."""

def __init__(self):
"""Initialize configuration."""
self.version: str = "1.0"
self.release: str = ""
self.deployments: Dict[str, Deployment] = {}
self._path_map: Dict[str, PathData] = {}
self._backend = SimpleValueBackend() # For non-sensitive values

@classmethod
def _load_schema(cls) -> Dict[str, Any]:
"""Load the JSON schema for configuration validation."""
schema_path = os.path.join(os.path.dirname(__file__), "..", "schemas", "v1.json")
with open(schema_path, "r", encoding="utf-8") as f:
return json.load(f)

def add_config_path(
self, path: str, description: Optional[str] = None, required: bool = False, sensitive: bool = False
) -> None:
"""
Add a new configuration path.

Args:
path: The configuration path
description: Description of what this configuration does
required: Whether this configuration is required
sensitive: Whether this configuration contains sensitive data
"""
if path in self._path_map:
raise ValueError(f"Path {path} already exists")

metadata = {
"description": description,
"required": required,
"sensitive": sensitive,
}
path_data = PathData(path, metadata)
self._path_map[path] = path_data

def get_value(self, path: str, environment: str, resolve: bool = False) -> str:
"""
Get a value for the given path and environment.

Args:
path: The configuration path
environment: The environment name
resolve: If True, resolve any secret references to their actual values.
If False, return the raw value which may be a secret reference.

Returns:
str: The value (resolved or raw depending on resolve parameter)

Raises:
KeyError: If path doesn't exist
ValueError: If value doesn't exist for the given environment
"""
if path not in self._path_map:
raise KeyError(f"Path {path} not found")

path_data = self._path_map[path]
value_obj = path_data.get_value(environment)
if value_obj is None:
raise ValueError(f"No value found for path {path} in environment {environment}")

value = value_obj.get(resolve=resolve)
if value is None:
raise ValueError(f"No value found for path {path} in environment {environment}")

return value

def set_value(self, path: str, environment: str, value: str) -> None:
"""Set a value for the given path and environment."""
if path not in self._path_map:
raise KeyError(f"Path {path} not found")

value_obj = Value(path=path, environment=environment, _backend=self._backend)
value_obj.set(value)
self._path_map[path].set_value(environment, value_obj)

def validate(self) -> None:
"""Validate the configuration."""
for path_data in self._path_map.values():
path_data.validate()

def to_dict(self) -> Dict[str, Any]:
"""Convert the configuration to a dictionary."""
return {
"version": self.version,
"release": self.release,
"deployments": {name: depl.to_dict() for name, depl in self.deployments.items()},
"config": [path_data.to_dict() for path_data in self._path_map.values()],
}

@classmethod
def from_dict(cls, data: dict) -> "HelmValuesConfig":
"""
Create a configuration from a dictionary.

Args:
data: Dictionary containing configuration data

Returns:
HelmValuesConfig: New configuration instance

Raises:
ValidationError: If the configuration data is invalid
"""
# Convert string boolean values to actual booleans for backward compatibility
data = data.copy() # Don't modify the input
for config_item in data.get("config", []):
for boolean_field in ["required", "sensitive"]:
if boolean_field in config_item and isinstance(config_item[boolean_field], str):
config_item[boolean_field] = config_item[boolean_field].lower() == "true"

# Validate against schema
schema = cls._load_schema()
try:
jsonschema.validate(instance=data, schema=schema)
except ValidationError as e:
raise e

config = cls()
config.version = data["version"]
config.release = data["release"]

# Load deployments
for name, depl_data in data.get("deployments", {}).items():
config.deployments[name] = Deployment(
name=name,
backend=depl_data["backend"],
auth=depl_data["auth"],
backend_config=depl_data.get("backend_config", {}),
)

# Load config paths
for config_item in data.get("config", []):
path = config_item["path"]
metadata = {
"description": config_item.get("description"),
"required": config_item.get("required", False),
"sensitive": config_item.get("sensitive", False),
}
path_data = PathData(path, metadata)
config._path_map[path] = path_data

# Load values
for env, value in config_item.get("values", {}).items():
value_obj = Value(path=path, environment=env, _backend=config._backend)
value_obj.set(value)
path_data.set_value(env, value_obj)

return config
Loading