|
| 1 | +# Validate Command Should Not Check SSH Key File Existence |
| 2 | + |
| 3 | +**Issue**: #340 |
| 4 | +**Parent Epic**: N/A |
| 5 | +**Related**: [Validate Command Documentation](../user-guide/commands/validate.md) |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +The `validate` command currently checks whether SSH key files exist on the filesystem. This is incorrect - the validate command should only perform **syntactic validation** (checking that paths are absolute), not **runtime validation** (checking that files exist). |
| 10 | + |
| 11 | +**Current Behavior**: |
| 12 | + |
| 13 | +- ✅ Correctly rejects relative paths: `fixtures/testing_rsa` → Error: "SSH private key path must be absolute" |
| 14 | +- ❌ **Bug**: Rejects absolute paths if files don't exist: `/home/user/.ssh/id_rsa` → Error: "SSH private key file not found" |
| 15 | + |
| 16 | +**Expected Behavior**: The validate command should: |
| 17 | + |
| 18 | +- ✅ Reject relative paths like `fixtures/testing_rsa` (already working) |
| 19 | +- ✅ Accept absolute paths like `/home/user/.ssh/id_rsa` **without checking if files exist** |
| 20 | +- ⚠️ File existence validation should only happen in `create environment` command |
| 21 | + |
| 22 | +**Rationale**: The validate command performs syntactic validation only - it checks configuration structure and constraints in isolation. File existence is a runtime validation that depends on the current system state and should only be checked when actually creating the environment. |
| 23 | + |
| 24 | +## Goals |
| 25 | + |
| 26 | +- [ ] Remove file existence check from validate command |
| 27 | +- [ ] Keep absolute path validation (already working correctly) |
| 28 | +- [ ] Ensure file existence check only happens in `create environment` command |
| 29 | +- [ ] Add unit tests confirming validation passes for non-existent absolute paths |
| 30 | +- [ ] Update E2E tests to verify both behaviors |
| 31 | + |
| 32 | +## 🏗️ Architecture Requirements |
| 33 | + |
| 34 | +**DDD Layer**: Application |
| 35 | +**Module Path**: `src/application/command_handlers/validate/` or `src/application/command_handlers/create/config/ssh_credentials_config.rs` |
| 36 | +**Pattern**: DTO validation / Command handler |
| 37 | + |
| 38 | +### Module Structure Requirements |
| 39 | + |
| 40 | +- [ ] Follow DDD layer separation (validation may happen in DTO → Domain conversion) |
| 41 | +- [ ] Respect dependency flow rules (Application → Domain) |
| 42 | +- [ ] Use appropriate error types (see [docs/contributing/error-handling.md](../docs/contributing/error-handling.md)) |
| 43 | + |
| 44 | +### Architectural Constraints |
| 45 | + |
| 46 | +- [ ] Validation logic should be testable in isolation |
| 47 | +- [ ] Error messages must be actionable and user-friendly (see [docs/development-principles.md](../docs/development-principles.md)) |
| 48 | +- [ ] No file I/O during validate command (syntactic validation only) |
| 49 | + |
| 50 | +### Anti-Patterns to Avoid |
| 51 | + |
| 52 | +- ❌ Checking file existence in validate command (should only be in create environment) |
| 53 | +- ❌ Generic error messages without actionable guidance |
| 54 | +- ❌ Mixing syntactic and runtime validation concerns |
| 55 | + |
| 56 | +## Specifications |
| 57 | + |
| 58 | +### Current Validation Behavior |
| 59 | + |
| 60 | +The validate command currently performs TWO checks: |
| 61 | + |
| 62 | +1. ✅ **Path must be absolute** - Working correctly, should be kept |
| 63 | +2. ❌ **File must exist** - Bug: This should only happen in create environment command |
| 64 | + |
| 65 | +**Test Results:** |
| 66 | + |
| 67 | +```bash |
| 68 | +# Test 1: Relative paths (correctly rejected) |
| 69 | +$ cargo run -- validate --env-file envs/bug-validation-invalid-relative-paths.json |
| 70 | +❌ Error: SSH private key path must be absolute: "fixtures/testing_rsa" |
| 71 | + |
| 72 | +# Test 2: Absolute paths to non-existent files (incorrectly rejected - this is the bug) |
| 73 | +$ cargo run -- validate --env-file envs/bug-validation-valid-absolute-paths.json |
| 74 | +❌ Error: SSH private key file not found: /home/user/.ssh/id_rsa |
| 75 | +``` |
| 76 | + |
| 77 | +### Required Fix |
| 78 | + |
| 79 | +**Remove file existence check from validate command** while keeping the absolute path validation. |
| 80 | + |
| 81 | +**After fix, expected behavior:** |
| 82 | + |
| 83 | +```bash |
| 84 | +# Test 1: Relative paths (should still be rejected) |
| 85 | +$ cargo run -- validate --env-file envs/bug-validation-invalid-relative-paths.json |
| 86 | +❌ Error: SSH private key path must be absolute: "fixtures/testing_rsa" |
| 87 | + |
| 88 | +# Test 2: Absolute paths to non-existent files (should now pass) |
| 89 | +$ cargo run -- validate --env-file envs/bug-validation-valid-absolute-paths.json |
| 90 | +✅ Configuration file 'envs/bug-validation-valid-absolute-paths.json' is valid |
| 91 | +``` |
| 92 | + |
| 93 | +### Test Configurations |
| 94 | + |
| 95 | +Two minimal test configurations are provided in `envs/`: |
| 96 | + |
| 97 | +**Valid (absolute paths)**: [`envs/bug-validation-valid-absolute-paths.json`](../../envs/bug-validation-valid-absolute-paths.json) |
| 98 | + |
| 99 | +```json |
| 100 | +{ |
| 101 | + "environment": { |
| 102 | + "name": "bug-test-valid" |
| 103 | + }, |
| 104 | + "ssh_credentials": { |
| 105 | + "private_key_path": "/home/user/.ssh/id_rsa", |
| 106 | + "public_key_path": "/home/user/.ssh/id_rsa.pub" |
| 107 | + }, |
| 108 | + "provider": { |
| 109 | + "provider": "lxd", |
| 110 | + "profile_name": "torrust-profile-bug-test" |
| 111 | + }, |
| 112 | + "tracker": { |
| 113 | + "core": { |
| 114 | + "database": { |
| 115 | + "driver": "sqlite3", |
| 116 | + "database_name": "tracker.db" |
| 117 | + }, |
| 118 | + "private": false |
| 119 | + }, |
| 120 | + "udp_trackers": [], |
| 121 | + "http_trackers": [], |
| 122 | + "http_api": { |
| 123 | + "bind_address": "0.0.0.0:1212", |
| 124 | + "admin_token": "MyAccessToken" |
| 125 | + }, |
| 126 | + "health_check_api": { |
| 127 | + "bind_address": "127.0.0.1:1313" |
| 128 | + } |
| 129 | + } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +**Invalid (relative paths)**: [`envs/bug-validation-invalid-relative-paths.json`](../../envs/bug-validation-invalid-relative-paths.json) |
| 134 | + |
| 135 | +```json |
| 136 | +{ |
| 137 | + "environment": { |
| 138 | + "name": "bug-test-invalid" |
| 139 | + }, |
| 140 | + "ssh_credentials": { |
| 141 | + "private_key_path": "fixtures/testing_rsa", |
| 142 | + "public_key_path": "fixtures/testing_rsa.pub" |
| 143 | + }, |
| 144 | + "provider": { |
| 145 | + "provider": "lxd", |
| 146 | + "profile_name": "torrust-profile-bug-test" |
| 147 | + }, |
| 148 | + "tracker": { |
| 149 | + "core": { |
| 150 | + "database": { |
| 151 | + "driver": "sqlite3", |
| 152 | + "database_name": "tracker.db" |
| 153 | + }, |
| 154 | + "private": false |
| 155 | + }, |
| 156 | + "udp_trackers": [], |
| 157 | + "http_trackers": [], |
| 158 | + "http_api": { |
| 159 | + "bind_address": "0.0.0.0:1212", |
| 160 | + "admin_token": "MyAccessToken" |
| 161 | + }, |
| 162 | + "health_check_api": { |
| 163 | + "bind_address": "127.0.0.1:1313" |
| 164 | + } |
| 165 | + } |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +### How to Reproduce |
| 170 | + |
| 171 | +```bash |
| 172 | +# Test 1: Relative paths (correctly rejected) |
| 173 | +cargo run --bin torrust-tracker-deployer -- validate --env-file envs/bug-validation-invalid-relative-paths.json |
| 174 | +# Expected: ❌ Error: SSH private key path must be absolute: "fixtures/testing_rsa" |
| 175 | +# Actual: ❌ Error: SSH private key path must be absolute: "fixtures/testing_rsa" ✓ |
| 176 | + |
| 177 | +# Test 2: Absolute paths to non-existent files (BUG - incorrectly rejected) |
| 178 | +cargo run --bin torrust-tracker-deployer -- validate --env-file envs/bug-validation-valid-absolute-paths.json |
| 179 | +# Expected: ✅ Configuration file is valid |
| 180 | +# Actual: ❌ Error: SSH private key file not found: /home/user/.ssh/id_rsa (BUG) |
| 181 | +``` |
| 182 | + |
| 183 | +### Implementation Location |
| 184 | + |
| 185 | +The file existence check is likely in one of these locations: |
| 186 | + |
| 187 | +1. `src/adapters/ssh/credentials.rs` - SshCredentials construction |
| 188 | +2. `src/application/command_handlers/validate/handler.rs` - Validate command handler |
| 189 | +3. `src/application/command_handlers/create/config/ssh_credentials_config.rs` - TryFrom conversion |
| 190 | + |
| 191 | +**Strategy**: Add a parameter or flag to skip file existence check when called from validate command, but still check when called from create environment command. |
| 192 | + |
| 193 | +## Implementation Plan |
| 194 | + |
| 195 | +### Phase 1: Locate File Existence Check (15 minutes) |
| 196 | + |
| 197 | +- [ ] Search codebase for file existence validation logic |
| 198 | +- [ ] Identify where `SshCredentials` checks if files exist |
| 199 | +- [ ] Determine if check is in domain layer, application layer, or adapter layer |
| 200 | +- [ ] Document current flow: validate command → ... → file existence check |
| 201 | + |
| 202 | +### Phase 2: Refactor to Skip File Check in Validate (45 minutes) |
| 203 | + |
| 204 | +- [ ] Add parameter/flag to skip file existence check (e.g., `validate_existence: bool`) |
| 205 | +- [ ] Update `SshCredentials::new()` or conversion method to accept flag |
| 206 | +- [ ] Validate command passes `validate_existence: false` |
| 207 | +- [ ] Create environment command passes `validate_existence: true` |
| 208 | +- [ ] Ensure absolute path validation still runs in both cases |
| 209 | + |
| 210 | +### Phase 3: Unit Tests (30 minutes) |
| 211 | + |
| 212 | +- [ ] Add test: `it_should_accept_absolute_paths_without_checking_existence` |
| 213 | +- [ ] Add test: `it_should_still_reject_relative_paths` |
| 214 | +- [ ] Add test: `it_should_check_file_existence_when_flag_is_true` (for create command) |
| 215 | +- [ ] Verify tests follow naming conventions from [docs/contributing/testing/unit-testing.md](../docs/contributing/testing/unit-testing.md) |
| 216 | + |
| 217 | +### Phase 4: E2E Tests (30 minutes) |
| 218 | + |
| 219 | +- [ ] Test using `bug-validation-valid-absolute-paths.json` (should now pass) |
| 220 | +- [ ] Test using `bug-validation-invalid-relative-paths.json` (should still fail) |
| 221 | +- [ ] Verify create environment command still checks file existence |
| 222 | +- [ ] Update `tests/e2e/validate_command.rs` with regression tests |
| 223 | + |
| 224 | +### Phase 5: Documentation (15 minutes) |
| 225 | + |
| 226 | +- [ ] Update [docs/user-guide/commands/validate.md](../user-guide/commands/validate.md) to clarify validation scope |
| 227 | +- [ ] Document that validate checks path format only, not file existence |
| 228 | +- [ ] Add example showing this distinction |
| 229 | +- [ ] Update error message examples if needed |
| 230 | + |
| 231 | +## Acceptance Criteria |
| 232 | + |
| 233 | +> **Note for Contributors**: These criteria define what the PR reviewer will check. Use this as your pre-review checklist before submitting the PR to minimize back-and-forth iterations. |
| 234 | +
|
| 235 | +**Quality Checks**: |
| 236 | + |
| 237 | +- [ ] Pre-commit checks pass: `./scripts/pre-commit.sh` |
| 238 | + |
| 239 | +**Task-Specific Criteria**: |
| 240 | + |
| 241 | +- [ ] ✅ Validate command accepts absolute SSH key paths without checking file existence |
| 242 | +- [ ] ❌ Validate command still rejects relative SSH key paths with clear error |
| 243 | +- [ ] ✅ Create environment command still checks if SSH key files exist |
| 244 | +- [ ] Unit tests cover both validate (no file check) and create (with file check) scenarios |
| 245 | +- [ ] E2E tests verify `bug-validation-valid-absolute-paths.json` now passes validation |
| 246 | +- [ ] E2E tests verify `bug-validation-invalid-relative-paths.json` still fails validation |
| 247 | +- [ ] Documentation clarifies validation scope (syntactic vs runtime checks) |
| 248 | + |
| 249 | +## Related Documentation |
| 250 | + |
| 251 | +- [Validate Command Documentation](../user-guide/commands/validate.md) |
| 252 | +- [Error Handling Guide](../contributing/error-handling.md) |
| 253 | +- [Unit Testing Conventions](../contributing/testing/unit-testing.md) |
| 254 | +- [Config Validation Feature Questions](../features/config-validation-command/questions.md) |
| 255 | + |
| 256 | +## Notes |
| 257 | + |
| 258 | +### What Was Discovered |
| 259 | + |
| 260 | +Testing revealed that the validate command already correctly validates that SSH paths must be absolute. The bug is that it **also checks file existence**, which it shouldn't do. |
| 261 | + |
| 262 | +**Test Results**: |
| 263 | + |
| 264 | +- `bug-validation-invalid-relative-paths.json` → Correctly fails: "SSH private key path must be absolute" |
| 265 | +- `bug-validation-valid-absolute-paths.json` → Incorrectly fails: "SSH private key file not found" |
| 266 | + |
| 267 | +The second case is the bug - it should pass validation because the path is absolute, regardless of whether the file exists. |
| 268 | + |
| 269 | +### Why Not Check File Existence in Validate? |
| 270 | + |
| 271 | +The validate command performs **syntactic validation** only - it checks the configuration structure and field constraints in isolation. File existence is a **runtime validation** that depends on the current system state and should only be checked when actually creating the environment. |
| 272 | + |
| 273 | +This separation aligns with the three levels of validation documented in [docs/features/config-validation-command/questions.md](../features/config-validation-command/questions.md): |
| 274 | + |
| 275 | +1. **Syntactic** ✅ - JSON structure, types, required fields, absolute paths (path format) |
| 276 | +2. **Config-intrinsic semantics** ✅ - Cross-field rules (e.g., Grafana requires Prometheus) |
| 277 | +3. **State-dependent semantics** ❌ - Depends on current app/system state (file existence, environment name conflicts) |
| 278 | + |
| 279 | +The validate command implements levels 1 and 2, while level 3 is deferred to the create environment command. |
| 280 | + |
| 281 | +### Real-World Use Case |
| 282 | + |
| 283 | +A common workflow is: |
| 284 | + |
| 285 | +1. User writes config on Machine A with SSH keys at `/home/alice/.ssh/id_rsa` |
| 286 | +2. User validates config on Machine A (should pass) |
| 287 | +3. User commits config to git repository |
| 288 | +4. CI/CD pipeline validates config on Machine B where `/home/alice/.ssh/id_rsa` doesn't exist (should still pass syntactic validation) |
| 289 | +5. Deployment runs on Machine C where the actual SSH keys exist (file existence checked here) |
| 290 | + |
| 291 | +The current behavior breaks step 4 - CI/CD validation incorrectly fails even though the config syntax is correct. |
0 commit comments