Skip to content

Commit c7a8dfd

Browse files
authored
Merge pull request #16 from Zipstack/feature/command-registration
feat: implement command registry and init command
2 parents d012ce9 + 98b7935 commit c7a8dfd

File tree

13 files changed

+478
-187
lines changed

13 files changed

+478
-187
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ ipython_config.py
122122
__pypackages__/
123123

124124
# Helm Values Manager specific
125+
helm-values.json
125126
.helm-values.lock
126127

127128
# Celery stuff
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# 8. Remove Command Registry Pattern
2+
3+
Date: 2025-02-26
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
We initially implemented a Command Registry pattern to manage and discover commands in our Helm plugin. The registry was designed to:
12+
- Register commands with unique names
13+
- Retrieve command instances by name
14+
- Potentially support future features like plugins, middleware, and dynamic command loading
15+
16+
However, our current implementation shows that:
17+
1. Commands are statically defined in CLI functions using Typer
18+
2. Each CLI function directly knows which command to instantiate
19+
3. We don't have requirements for:
20+
- Plugin system or third-party extensions
21+
- Dynamic command loading/discovery
22+
- Command aliasing or middleware
23+
- Runtime command configuration
24+
25+
The registry adds an unnecessary layer of indirection:
26+
```python
27+
# Current approach with registry
28+
registry = CommandRegistry()
29+
registry.register("init", InitCommand)
30+
command = registry.get_command("init")()
31+
32+
# Simplified to
33+
command = InitCommand()
34+
```
35+
36+
## Decision
37+
38+
We will remove the Command Registry pattern and use direct command instantiation because:
39+
1. It simplifies the code by removing unnecessary abstraction
40+
2. It follows YAGNI (You Ain't Gonna Need It) principle
41+
3. Our commands are fixed and well-defined as part of the Helm plugin
42+
4. Typer handles CLI command registration and discovery
43+
5. We don't have current or planned requirements that would benefit from a registry
44+
45+
## Consequences
46+
47+
### Positive
48+
1. Simpler, more maintainable code
49+
2. Less indirection and complexity
50+
3. Easier to understand command flow
51+
4. Reduced testing surface
52+
5. Better alignment with Typer's design
53+
54+
### Negative
55+
1. If we need these features in the future, we'll need to:
56+
- Reimplement the registry pattern
57+
- Update all command instantiation points
58+
- Add command discovery mechanism
59+
60+
### Neutral
61+
1. Command implementation and interface remain unchanged
62+
2. CLI functionality remains the same
63+
3. User experience is unaffected
64+
65+
## Future Considerations
66+
67+
If we need registry features in the future, we should consider:
68+
1. Plugin system requirements
69+
2. Command discovery needs
70+
3. Middleware requirements
71+
4. Integration with Helm plugin system
72+
73+
## Related
74+
- [ADR-001] Initial Architecture Decision

docs/ADRs/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ An Architecture Decision Record (ADR) is a document that captures an important a
5555
- **Impact**: Ensures security while maintaining flexibility and traceability
5656
- **Dependencies**: ADR-005
5757

58+
### [ADR-008: Remove Command Registry](008-remove-command-registry.md)
59+
- **Status**: Accepted
60+
- **Context**: Command Registry pattern adds unnecessary complexity
61+
- **Decision**: Remove registry in favor of direct command instantiation
62+
- **Impact**: Simplifies code and aligns better with Typer's design
63+
5864
## ADR Template
5965
For new ADRs, use this template:
6066
```markdown

docs/Development/tasks.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,14 @@
8181
- [x] Implement BaseCommand class with basic flow
8282
- [x] Add configuration loading/saving
8383
- [x] Add error handling and logging
84-
- [ ] Add command registration in CLI
85-
- [ ] Add basic command discovery
84+
- [x] Add command registration in CLI
85+
- [x] Add basic command discovery
8686

87-
- [ ] Configuration Setup Commands
88-
- [ ] Implement init command
89-
- [ ] Add empty config initialization
90-
- [ ] Add config file creation
91-
- [ ] Add schema template generation
87+
- [x] Configuration Setup Commands
88+
- [x] Implement init command
89+
- [x] Add empty config initialization
90+
- [x] Add config file creation
91+
- [x] Add schema template generation
9292
- [ ] Implement add-value-config command
9393
- [ ] Add basic path validation
9494
- [ ] Add metadata validation
@@ -110,9 +110,9 @@
110110
- [ ] Add value storage
111111

112112
#### Phase 2: Enhanced Safety & Management
113-
- [ ] Enhanced Command Infrastructure
114-
- [ ] Add file locking mechanism
115-
- [ ] Add atomic writes
113+
- [x] Enhanced Command Infrastructure
114+
- [x] Add file locking mechanism
115+
- [x] Add atomic writes
116116
- [ ] Add basic backup strategy
117117

118118
- [ ] Configuration Management
@@ -148,11 +148,11 @@
148148
- [ ] Add help text improvements
149149
- [ ] Add usage examples
150150

151-
- [ ] Testing Infrastructure
152-
- [ ] Add command test fixtures
153-
- [ ] Add mock file system
154-
- [ ] Add mock backend
155-
- [ ] Add integration tests
151+
- [x] Testing Infrastructure
152+
- [x] Add command test fixtures
153+
- [x] Add mock file system
154+
- [x] Add mock backend
155+
- [x] Add integration tests
156156

157157
- [ ] Final Touches
158158
- [ ] Add command output formatting
@@ -170,10 +170,10 @@
170170
- [x] ConfigMetadata tests
171171
- [ ] Backend tests
172172
- [ ] Command tests
173-
- [ ] Add integration tests
174-
- [ ] End-to-end command tests
175-
- [ ] Backend integration tests
176-
- [ ] File operation tests
173+
- [x] Add integration tests
174+
- [x] End-to-end command tests
175+
- [x] Backend integration tests
176+
- [x] File operation tests
177177

178178
### Logging System
179179
- [x] Implement Helm-style logger

helm_values_manager/cli.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import typer
44

5+
from helm_values_manager.commands.init_command import InitCommand
6+
from helm_values_manager.utils.logger import HelmLogger
7+
58
COMMAND_INFO = "helm values-manager"
69

710
app = typer.Typer(
@@ -28,16 +31,15 @@ def main(ctx: typer.Context):
2831
@app.command()
2932
def init(
3033
release_name: str = typer.Option(..., "--release", "-r", help="Name of the Helm release"),
31-
config_file: str = typer.Option(
32-
"values-manager.yaml",
33-
"--config",
34-
"-c",
35-
help="Path to the values manager configuration file",
36-
),
3734
):
3835
"""Initialize a new values manager configuration."""
39-
typer.echo(f"Initializing values manager with config file: {config_file}, for the release: {release_name}.")
40-
# TODO: Implement initialization logic
36+
try:
37+
command = InitCommand()
38+
result = command.execute(release_name=release_name)
39+
typer.echo(result)
40+
except Exception as e:
41+
HelmLogger.error("Failed to initialize: %s", str(e))
42+
raise typer.Exit(code=1) from e
4143

4244

4345
if __name__ == "__main__":

helm_values_manager/commands/base_command.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,19 @@ def release_lock(self) -> None:
108108
self._lock_fd = None
109109
HelmLogger.debug("Released lock on file %s", self.lock_file)
110110

111-
def execute(self) -> Any:
111+
def execute(self, **kwargs) -> Any:
112112
"""Execute the command.
113113
114114
This is the main entry point for running a command.
115115
It handles:
116116
1. Lock acquisition
117-
2. Configuration loading
117+
2. Configuration loading (if needed)
118118
3. Command execution via run()
119119
4. Lock release
120120
121+
Args:
122+
**kwargs: Command-specific keyword arguments
123+
121124
Returns:
122125
Any: The result of the command execution.
123126
@@ -126,20 +129,23 @@ def execute(self) -> Any:
126129
"""
127130
try:
128131
self.acquire_lock()
129-
config = self.load_config()
130-
result = self.run(config)
132+
config = None
133+
if not getattr(self, "skip_config_load", False):
134+
config = self.load_config()
135+
result = self.run(config=config, **kwargs)
131136
return result
132137
finally:
133138
self.release_lock()
134139

135-
def run(self, config: HelmValuesConfig) -> Any:
140+
def run(self, config: Optional[HelmValuesConfig] = None, **kwargs) -> Any:
136141
"""Run the command-specific logic.
137142
138143
This method should be implemented by each specific command subclass
139144
to perform its unique functionality.
140145
141146
Args:
142-
config: The loaded configuration.
147+
config: The loaded configuration, or None if skip_config_load is True
148+
**kwargs: Command-specific keyword arguments
143149
144150
Returns:
145151
Any: The result of running the command.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Command to initialize a new helm-values configuration."""
2+
3+
import os
4+
from typing import Optional
5+
6+
from helm_values_manager.commands.base_command import BaseCommand
7+
from helm_values_manager.models.helm_values_config import HelmValuesConfig
8+
from helm_values_manager.utils.logger import HelmLogger
9+
10+
11+
class InitCommand(BaseCommand):
12+
"""Command to initialize a new helm-values configuration."""
13+
14+
def __init__(self) -> None:
15+
"""Initialize the init command."""
16+
super().__init__()
17+
self.skip_config_load = True
18+
19+
def run(self, config: Optional[HelmValuesConfig] = None, **kwargs) -> str:
20+
"""
21+
Initialize a new helm-values configuration.
22+
23+
Args:
24+
config: Not used by this command
25+
**kwargs: Command arguments
26+
- release_name (str): Name of the Helm release
27+
28+
Returns:
29+
str: Success message
30+
31+
Raises:
32+
FileExistsError: If configuration file already exists
33+
ValueError: If release name is invalid
34+
"""
35+
release_name = kwargs.get("release_name")
36+
if not release_name:
37+
raise ValueError("Release name cannot be empty")
38+
39+
# Check if config file already exists
40+
if os.path.exists(self.config_file):
41+
raise FileExistsError(f"Configuration file {self.config_file} already exists")
42+
43+
# Create new config
44+
new_config = HelmValuesConfig()
45+
new_config.version = "1.0"
46+
new_config.release = release_name
47+
48+
# Save config
49+
self.save_config(new_config)
50+
51+
HelmLogger.debug("Initialized helm-values configuration for release: %s", release_name)
52+
return "Successfully initialized helm-values configuration."

helm_values_manager/models/helm_values_config.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,16 @@ def set_value(self, path: str, environment: str, value: str) -> None:
131131
self._path_map[path].set_value(environment, value_obj)
132132

133133
def validate(self) -> None:
134-
"""Validate the configuration."""
135-
# Validate against JSON schema first
134+
"""
135+
Validate the configuration.
136+
137+
Raises:
138+
ValueError: If validation fails.
139+
"""
140+
if not self.release:
141+
raise ValueError("Release name is required")
142+
143+
# Validate schema
136144
self._validate_schema(self.to_dict())
137145

138146
# Then validate each path_data

tests/integration/test_cli_integration.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Integration tests for the helm-values-manager CLI."""
22

3+
import os
34
import subprocess
45
from pathlib import Path
56

@@ -70,13 +71,17 @@ def test_init_help_command(plugin_install):
7071

7172
assert returncode == 0
7273
assert "Initialize a new values manager configuration" in stdout
73-
assert "--config" in stdout
74-
assert "-c" in stdout
74+
assert "--release" in stdout
75+
assert "-r" in stdout
7576

7677

7778
def test_init_command(plugin_install, tmp_path):
78-
"""Test that the init command works with default config file."""
79+
"""Test that the init command works."""
80+
# Change to temp directory to avoid conflicts
81+
os.chdir(tmp_path)
7982
stdout, stderr, returncode = run_helm_command(["values-manager", "init", "-r", "test"])
8083

8184
assert returncode == 0
82-
assert "Initializing values manager with config file: values-manager.yaml, for the release: test." in stdout
85+
assert "Successfully initialized helm-values configuration" in stdout
86+
assert Path("helm-values.json").exists()
87+
assert Path(".helm-values.lock").exists()

0 commit comments

Comments
 (0)