Skip to content

Commit da591fc

Browse files
authored
Merge pull request #15 from Zipstack/feature/add-base-command
feat: Add BaseCommand class for command handling
2 parents 7ddb572 + dc94788 commit da591fc

File tree

6 files changed

+407
-10
lines changed

6 files changed

+407
-10
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ ipython_config.py
121121
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
122122
__pypackages__/
123123

124+
# Helm Values Manager specific
125+
.helm-values.lock
126+
124127
# Celery stuff
125128
celerybeat-schedule
126129
celerybeat.pid

docs/Development/tasks.md

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,90 @@
7474
- [ ] Add better validation
7575

7676
### Command System
77-
- [ ] Implement BaseCommand improvements
78-
- [ ] Add file locking mechanism
79-
- [ ] Implement backup strategy
80-
- [ ] Add better error handling
81-
- [ ] Add new commands
77+
78+
#### Phase 1: Core Infrastructure & Essential Commands
79+
- [x] Basic Command Framework
80+
- [x] Create commands directory structure
81+
- [x] Implement BaseCommand class with basic flow
82+
- [x] Add configuration loading/saving
83+
- [x] Add error handling and logging
84+
- [ ] Add command registration in CLI
85+
- [ ] Add basic command discovery
86+
87+
- [ ] Configuration Setup Commands
88+
- [ ] Implement init command
89+
- [ ] Add empty config initialization
90+
- [ ] Add config file creation
91+
- [ ] Add schema template generation
8292
- [ ] Implement add-value-config command
93+
- [ ] Add basic path validation
94+
- [ ] Add metadata validation
95+
- [ ] Add config update
96+
- [ ] Implement add-deployment command
97+
- [ ] Add basic deployment validation
98+
- [ ] Add backend validation
99+
- [ ] Add deployment registration
100+
- [ ] Implement generate command
101+
- [ ] Add template generation
102+
- [ ] Add basic value substitution
103+
104+
- [ ] Value Management Commands
105+
- [ ] Implement get-value command
106+
- [ ] Add basic path validation
107+
- [ ] Add value retrieval
108+
- [ ] Implement set-value command
109+
- [ ] Add basic path validation
110+
- [ ] Add value storage
111+
112+
#### Phase 2: Enhanced Safety & Management
113+
- [ ] Enhanced Command Infrastructure
114+
- [ ] Add file locking mechanism
115+
- [ ] Add atomic writes
116+
- [ ] Add basic backup strategy
117+
118+
- [ ] Configuration Management
83119
- [ ] Implement remove-value-config command
84-
- [ ] Update existing commands for new structure
85-
- [ ] Update command validation
86-
- [ ] Add input validation
87-
- [ ] Improve error messages
88-
- [ ] Add command-specific validation
120+
- [ ] Add path validation
121+
- [ ] Add basic cleanup
122+
- [ ] Enhance add-value-config
123+
- [ ] Add conflict detection
124+
- [ ] Add dependency validation
125+
126+
- [ ] Basic Validation System
127+
- [ ] Add PathValidator class
128+
- [ ] Add path format validation
129+
- [ ] Add existence checks
130+
131+
#### Phase 3: Advanced Features
132+
- [ ] Enhanced Security & Recovery
133+
- [ ] Add comprehensive backup strategy
134+
- [ ] Add rollback support
135+
- [ ] Improve error handling
136+
137+
- [ ] Deployment Management
138+
- [ ] Add DeploymentValidator class
139+
140+
- [ ] Advanced Validation
141+
- [ ] Add ValueValidator class
142+
- [ ] Add conflict detection
143+
- [ ] Add dependency checking
144+
145+
#### Phase 4: Polish & Documentation
146+
- [ ] Command Documentation
147+
- [ ] Add command documentation generation
148+
- [ ] Add help text improvements
149+
- [ ] Add usage examples
150+
151+
- [ ] Testing Infrastructure
152+
- [ ] Add command test fixtures
153+
- [ ] Add mock file system
154+
- [ ] Add mock backend
155+
- [ ] Add integration tests
156+
157+
- [ ] Final Touches
158+
- [ ] Add command output formatting
159+
- [ ] Add progress indicators
160+
- [ ] Add interactive mode support
89161

90162
### Testing Infrastructure
91163
- [x] Set up test infrastructure
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Commands package containing command implementations for the helm-values-manager."""
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""Base command class for helm-values plugin."""
2+
3+
import fcntl
4+
import json
5+
import os
6+
from typing import Any, Optional
7+
8+
from jsonschema.exceptions import ValidationError
9+
10+
from helm_values_manager.models.helm_values_config import HelmValuesConfig
11+
from helm_values_manager.utils.logger import HelmLogger
12+
13+
14+
class BaseCommand:
15+
"""Base class for all helm-values commands.
16+
17+
This class provides common functionality for all commands including:
18+
- Configuration loading and saving
19+
- Error handling and logging
20+
- Lock management for concurrent access
21+
"""
22+
23+
def __init__(self) -> None:
24+
"""Initialize the base command."""
25+
self.config_file = "helm-values.json"
26+
self.lock_file = ".helm-values.lock"
27+
self._lock_fd: Optional[int] = None
28+
29+
def _load_config_file(self) -> dict:
30+
"""Load and parse the configuration file.
31+
32+
Returns:
33+
dict: The parsed configuration data
34+
35+
Raises:
36+
FileNotFoundError: If the config file doesn't exist
37+
json.JSONDecodeError: If the file contains invalid JSON
38+
"""
39+
if not os.path.exists(self.config_file):
40+
HelmLogger.error("Configuration file %s not found", self.config_file)
41+
raise FileNotFoundError(f"Configuration file {self.config_file} not found")
42+
43+
try:
44+
with open(self.config_file, "r", encoding="utf-8") as f:
45+
return json.load(f)
46+
except json.JSONDecodeError as e:
47+
HelmLogger.error("Failed to parse configuration file: %s", e)
48+
raise
49+
50+
def load_config(self) -> HelmValuesConfig:
51+
"""Load the helm-values configuration from disk.
52+
53+
Returns:
54+
HelmValuesConfig: The loaded configuration.
55+
56+
Raises:
57+
FileNotFoundError: If the config file doesn't exist.
58+
json.JSONDecodeError: If the file contains invalid JSON.
59+
ValidationError: If the configuration format is invalid.
60+
"""
61+
data = self._load_config_file()
62+
try:
63+
return HelmValuesConfig.from_dict(data)
64+
except ValidationError as e:
65+
HelmLogger.error("Invalid configuration format: %s", e)
66+
raise
67+
68+
def save_config(self, config: HelmValuesConfig) -> None:
69+
"""Save the helm-values configuration to disk.
70+
71+
Args:
72+
config: The configuration to save.
73+
74+
Raises:
75+
IOError: If unable to write to the file.
76+
"""
77+
try:
78+
with open(self.config_file, "w", encoding="utf-8") as f:
79+
json.dump(config.to_dict(), f, indent=2)
80+
except IOError as e:
81+
HelmLogger.error("Failed to save configuration: %s", e)
82+
raise
83+
84+
def acquire_lock(self) -> None:
85+
"""Acquire an exclusive lock for file operations.
86+
87+
This ensures thread-safe operations when multiple commands
88+
are trying to modify the configuration.
89+
90+
Raises:
91+
IOError: If unable to acquire the lock.
92+
"""
93+
self._lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_RDWR)
94+
try:
95+
fcntl.flock(self._lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
96+
HelmLogger.debug("Acquired lock on file %s", self.lock_file)
97+
except IOError:
98+
os.close(self._lock_fd)
99+
self._lock_fd = None
100+
HelmLogger.error("Unable to acquire lock. Another command may be running.")
101+
raise IOError("Unable to acquire lock. Another command may be running.")
102+
103+
def release_lock(self) -> None:
104+
"""Release the exclusive lock."""
105+
if self._lock_fd is not None:
106+
fcntl.flock(self._lock_fd, fcntl.LOCK_UN)
107+
os.close(self._lock_fd)
108+
self._lock_fd = None
109+
HelmLogger.debug("Released lock on file %s", self.lock_file)
110+
111+
def execute(self) -> Any:
112+
"""Execute the command.
113+
114+
This is the main entry point for running a command.
115+
It handles:
116+
1. Lock acquisition
117+
2. Configuration loading
118+
3. Command execution via run()
119+
4. Lock release
120+
121+
Returns:
122+
Any: The result of the command execution.
123+
124+
Raises:
125+
Exception: If any error occurs during command execution.
126+
"""
127+
try:
128+
self.acquire_lock()
129+
config = self.load_config()
130+
result = self.run(config)
131+
return result
132+
finally:
133+
self.release_lock()
134+
135+
def run(self, config: HelmValuesConfig) -> Any:
136+
"""Run the command-specific logic.
137+
138+
This method should be implemented by each specific command subclass
139+
to perform its unique functionality.
140+
141+
Args:
142+
config: The loaded configuration.
143+
144+
Returns:
145+
Any: The result of running the command.
146+
147+
Raises:
148+
NotImplementedError: This method must be implemented by subclasses.
149+
"""
150+
raise NotImplementedError("Subclasses must implement run()")

tests/unit/commands/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Unit tests for the commands package."""

0 commit comments

Comments
 (0)