Просмотр исходного кода

Added spec for feature addition

David Walshe 1 месяц назад
Родитель
Сommit
0e1a4e7eb4

+ 2 - 0
openspec/changes/add-key-value-labels/.openspec.yaml

@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-02-23

+ 90 - 0
openspec/changes/add-key-value-labels/design.md

@@ -0,0 +1,90 @@
+# Design: Add Key-Value Labels to Resources
+
+## Context
+
+RackPeek models IT infrastructure as a hierarchy of Resources (Hardware → System → Service). Resources currently have `Name`, `Tags` (string array), `Notes`, and `RunsOn`. Tags are single-value strings used for categorization (e.g., "production", "staging"); they are managed via `AddTagUseCase` / `RemoveTagUseCase` and displayed in `ResourceTagEditor`. Tags have no CLI commands today—they are Web UI only.
+
+Labels differ from Tags: they are key-value pairs (e.g., `env: production`, `owner: team-a`). Users need arbitrary metadata for organization, filtering, and reporting. The project overview and code-style rules already anticipate Labels (`Dictionary<string, string>` on `Resource`), but the implementation does not exist yet. `KeyValueModal` exists in Shared.Rcl and can be reused for label add/edit in the Web UI.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Add `Labels` property (`Dictionary<string, string>`) to `Resource` base class.
+- Implement `AddLabelUseCase` and `RemoveLabelUseCase` following the Tags pattern.
+- Persist labels in YAML; existing resources without labels deserialize as empty dict.
+- Expose add/remove label commands in CLI for each resource branch (servers, switches, etc.).
+- Display and edit labels in the Web UI via a `ResourceLabelEditor` component (reusing `KeyValueModal`).
+
+**Non-Goals:**
+
+- Label-based filtering or querying in CLI/Web (future enhancement).
+- Label validation beyond key/value presence and length limits.
+- Bulk label operations or import/export.
+
+## Decisions
+
+### 1. Data model: `Dictionary<string, string>` on Resource
+
+**Decision:** Add `Labels` as `Dictionary<string, string>` on `Resource`, initialized to `new()` (never null).
+
+**Rationale:** Matches Kubernetes-style labels and the project's code-style rules. Simple, serializable, and supports arbitrary key-value pairs.
+
+**Alternatives considered:** `List<(string, string)>` — rejected because lookup by key is common and Dictionary is more ergonomic. `Dictionary<string, string?>` — rejected; empty string is acceptable for value.
+
+### 2. Use case pattern: Mirror Tags
+
+**Decision:** Create `AddLabelUseCase<T>` and `RemoveLabelUseCase<T>` in `UseCases/Labels/`, following the same structure as `AddTagUseCase` / `RemoveTagUseCase`.
+
+**Rationale:** Consistency with existing patterns; DI registration via open generics; single responsibility per use case.
+
+**Alternatives considered:** Single `LabelUseCase` with Add/Remove methods — rejected per SRP. Reusing Tag use cases with a "mode" — rejected; labels have different semantics (key-value vs single value).
+
+### 3. Normalization and validation
+
+**Decision:** Normalize key and value with `Trim()`. Validate: key and value non-empty, key length ≤ 50, value length ≤ 200. Add `ThrowIfInvalid.LabelKey` and `ThrowIfInvalid.LabelValue` (or similar) helpers.
+
+**Rationale:** Prevents whitespace-only labels; keeps YAML readable; avoids unbounded storage.
+
+**Alternatives considered:** No length limits — rejected for YAML size and UX. Lowercase keys — rejected; user may want case-sensitive keys (e.g., `Env` vs `env`).
+
+### 4. CLI structure: `label add` / `label remove` per resource branch
+
+**Decision:** Add a `label` sub-branch under each resource branch (servers, switches, systems, etc.) with `add` and `remove` commands. Example: `rpk servers label add web-01 --key env --value prod`, `rpk servers label remove web-01 --key env`.
+
+**Rationale:** Matches the E2E example workflow; consistent with `cpu add`, `drive add` sub-branch pattern.
+
+**Alternatives considered:** Global `label add <resource-type> <name> ...` — rejected; less discoverable. Positional args only — rejected; `--key`/`--value` improve clarity for key-value semantics.
+
+### 5. YAML serialization
+
+**Decision:** Rely on YamlDotNet default serialization for `Dictionary<string, string>`. No custom converter unless ordering or format requires it.
+
+**Rationale:** YamlDotNet serializes `Dictionary<string, string>` as YAML mapping. Existing resources without `Labels` will deserialize with default `new()`; migration not required if property is initialized.
+
+**Alternatives considered:** Custom converter for key ordering — deferred; can add later if needed. Schema version bump — only if migration logic is required; adding a new optional property typically does not require it.
+
+### 6. Web UI: ResourceLabelEditor component
+
+**Decision:** Create `ResourceLabelEditor` in Shared.Rcl, mirroring `ResourceTagEditor`. Use `KeyValueModal` for add/edit. Display labels as key-value chips with remove button; add button opens modal.
+
+**Rationale:** Reuses existing `KeyValueModal`; consistent UX with Tags; shared across all resource types via `@typeparam TResource`.
+
+## Risks / Trade-offs
+
+| Risk | Mitigation |
+|------|-------------|
+| YAML files from older app versions lack `Labels` | Initialize to empty dict in Resource; YamlDotNet will populate or leave default. Verify deserialization of legacy YAML. |
+| Duplicate label commands across 10+ resource branches | Use shared command base or factory; CliBootstrap will have repetitive but explicit registration (consistent with existing pattern). |
+| Key/value injection in YAML (e.g., special chars) | YamlDotNet handles escaping; validate key/value to reject control characters if needed. |
+| Labels bloat YAML file size | Length limits (50/200) mitigate; bulk operations out of scope. |
+
+## Migration Plan
+
+- **Deploy:** No schema version bump required if `Labels` is an additive, optional property. Existing YAML without `labels:` will deserialize with empty dictionary.
+- **Rollback:** Revert code; YAML with `labels:` will be ignored by older app (YamlDotNet typically ignores unknown properties). If strict compatibility is required, add migration step to strip labels when downgrading.
+
+## Open Questions
+
+- Should `describe` output format labels differently from tags (e.g., `Labels: env=prod, owner=team-a` vs `Tags: production, staging`)? **Recommendation:** Yes; use `key: value` format for labels.
+- Should we support label filtering in `list`/`get` in this change? **Recommendation:** No; keep scope to add/remove and display.

+ 29 - 0
openspec/changes/add-key-value-labels/proposal.md

@@ -0,0 +1,29 @@
+# Proposal: Add Key-Value Labels to Resources
+
+## Why
+
+The user would like the ability to add arbitrary labels to each resource. This helps the user associate key-value information on their resources (e.g., environment, owner, cost-center) for organization, filtering, and reporting.
+
+## What Changes
+
+- Addition of Labels (key-value section) to each resource
+- Add/Remove label use cases for managing labels via CLI and Web UI
+- YAML persistence of labels on resources
+- Display and editing of labels in the Web UI
+
+## Capabilities
+
+### New Capabilities
+
+- `resource-labels`: User can associate key-value attributes to their various resources in RackPeek. Labels are arbitrary string key-value pairs stored on each resource, with add/remove operations exposed via CLI and Web UI.
+
+### Modified Capabilities
+
+- None (no existing specs in `openspec/specs/`)
+
+## Impact
+
+- **Domain**: `Resource` base class gains `Labels` property (`Dictionary<string, string>`); new `AddLabelUseCase` and `RemoveLabelUseCase` in `UseCases/Labels/`
+- **Persistence**: YAML schema must serialize/deserialize labels; existing resources without labels remain valid (empty dict)
+- **CLI**: New commands for adding/removing labels per resource type (e.g., `rpk servers add-label <name> --key <key> --value <value>`, `rpk servers remove-label <name> --key <key>`)
+- **Web UI**: Resource card and edit flows must display and allow editing of labels (e.g., `ResourceLabelEditor` component)

+ 130 - 0
openspec/changes/add-key-value-labels/specs/resource-labels/spec.md

@@ -0,0 +1,130 @@
+# Spec: Resource Labels
+
+## ADDED Requirements
+
+### Requirement: Resource stores labels as key-value pairs
+
+Each resource SHALL have a Labels property that stores arbitrary string key-value pairs. Labels SHALL be initialized to an empty collection and MUST NOT be null.
+
+#### Scenario: New resource has empty labels
+
+- **WHEN** a resource is created
+- **THEN** the resource has an empty Labels collection
+
+#### Scenario: Labels are stored per resource
+
+- **WHEN** a label with key "env" and value "production" is added to resource "web-01"
+- **THEN** resource "web-01" has Labels containing "env" -> "production"
+- **AND** other resources are unaffected
+
+### Requirement: User can add a label to a resource
+
+The system SHALL allow users to add a label (key-value pair) to any resource via CLI and Web UI.
+
+#### Scenario: Add label via CLI
+
+- **WHEN** user runs `rpk servers label add web-01 --key env --value production`
+- **THEN** resource "web-01" has label "env" with value "production"
+- **AND** the change is persisted to YAML
+
+#### Scenario: Add label via Web UI
+
+- **WHEN** user opens the resource card, clicks add label, enters key "owner" and value "team-a", and submits
+- **THEN** the resource has label "owner" with value "team-a"
+- **AND** the change is persisted
+
+#### Scenario: Add label to existing key overwrites value
+
+- **WHEN** resource "web-01" has label "env" with value "staging"
+- **AND** user adds label "env" with value "production"
+- **THEN** resource "web-01" has label "env" with value "production"
+
+### Requirement: User can remove a label from a resource
+
+The system SHALL allow users to remove a label by key from any resource via CLI and Web UI.
+
+#### Scenario: Remove label via CLI
+
+- **WHEN** resource "web-01" has label "env" with value "production"
+- **AND** user runs `rpk servers label remove web-01 --key env`
+- **THEN** resource "web-01" no longer has label "env"
+- **AND** the change is persisted to YAML
+
+#### Scenario: Remove label via Web UI
+
+- **WHEN** resource has label "owner" with value "team-a"
+- **AND** user clicks remove on that label in the resource card
+- **THEN** the resource no longer has label "owner"
+- **AND** the change is persisted
+
+#### Scenario: Remove nonexistent key is no-op
+
+- **WHEN** resource "web-01" does not have label "env"
+- **AND** user runs `rpk servers label remove web-01 --key env`
+- **THEN** no error occurs
+- **AND** the resource is unchanged
+
+### Requirement: Labels persist in YAML
+
+Labels SHALL be serialized and deserialized from the YAML resource file. Existing resources without a labels section SHALL deserialize with an empty Labels collection.
+
+#### Scenario: Labels are written to YAML
+
+- **WHEN** resource "web-01" has labels "env: production" and "owner: team-a"
+- **AND** the resource is saved
+- **THEN** the YAML file contains a labels section for that resource with the key-value pairs
+
+#### Scenario: Legacy YAML without labels deserializes correctly
+
+- **WHEN** resource in YAML has no labels section
+- **THEN** the resource deserializes with an empty Labels collection
+
+### Requirement: Labels are displayed in resource views
+
+The system SHALL display labels in resource describe output (CLI) and resource cards (Web UI).
+
+#### Scenario: Describe shows labels
+
+- **WHEN** resource "web-01" has labels "env: production" and "owner: team-a"
+- **AND** user runs `rpk servers describe web-01`
+- **THEN** the output includes the labels in key-value format
+
+#### Scenario: Resource card shows labels
+
+- **WHEN** resource has label "env" with value "production"
+- **AND** user views the resource card in Web UI
+- **THEN** the label is displayed
+
+### Requirement: Label key and value are validated
+
+The system SHALL validate label key and value before adding. Key and value MUST be non-empty after trimming. Key length MUST NOT exceed 50 characters. Value length MUST NOT exceed 200 characters.
+
+#### Scenario: Empty key is rejected
+
+- **WHEN** user attempts to add a label with empty or whitespace-only key
+- **THEN** a validation error is returned
+- **AND** the resource is not updated
+
+#### Scenario: Empty value is rejected
+
+- **WHEN** user attempts to add a label with empty or whitespace-only value
+- **THEN** a validation error is returned
+- **AND** the resource is not updated
+
+#### Scenario: Key exceeds length limit is rejected
+
+- **WHEN** user attempts to add a label with key longer than 50 characters
+- **THEN** a validation error is returned
+- **AND** the resource is not updated
+
+#### Scenario: Value exceeds length limit is rejected
+
+- **WHEN** user attempts to add a label with value longer than 200 characters
+- **THEN** a validation error is returned
+- **AND** the resource is not updated
+
+#### Scenario: Add label for nonexistent resource fails
+
+- **WHEN** user attempts to add a label to a resource that does not exist
+- **THEN** a not-found error is returned
+- **AND** no change is persisted

+ 44 - 0
openspec/changes/add-key-value-labels/tasks.md

@@ -0,0 +1,44 @@
+# Tasks: Add Key-Value Labels to Resources
+
+## 1. Domain Model and Validation
+
+- [ ] 1.1 Add `Labels` property (`Dictionary<string, string>`) to `Resource` base class, initialized to `new()`
+- [ ] 1.2 Add `Normalize.LabelKey` and `Normalize.LabelValue` helpers (trim)
+- [ ] 1.3 Add `ThrowIfInvalid.LabelKey` and `ThrowIfInvalid.LabelValue` (non-empty, key ≤ 50 chars, value ≤ 200 chars)
+- [ ] 1.4 Create `AddLabelUseCase<T>` and `IAddLabelUseCase<T>` in `UseCases/Labels/`
+- [ ] 1.5 Create `RemoveLabelUseCase<T>` and `IRemoveLabelUseCase<T>` in `UseCases/Labels/`
+- [ ] 1.6 Register `IAddLabelUseCase<>` and `IRemoveLabelUseCase<>` in `ServiceCollectionExtensions`
+
+## 2. Persistence
+
+- [ ] 2.1 Verify YamlDotNet serializes/deserializes `Dictionary<string, string>` for labels (no custom converter)
+- [ ] 2.2 Add unit test for YAML round-trip with labels and legacy YAML without labels
+
+## 3. CLI Commands
+
+- [ ] 3.1 Create `ServerLabelAddCommand` and `ServerLabelRemoveCommand` with `--key` and `--value` options
+- [ ] 3.2 Add `label` branch with `add` and `remove` commands to servers in CliBootstrap
+- [ ] 3.3 Add label add/remove commands for switches, routers, firewalls
+- [ ] 3.4 Add label add/remove commands for systems, accesspoints, ups
+- [ ] 3.5 Add label add/remove commands for desktops, laptops, services
+- [ ] 3.6 Update `ServerDescribeCommand` to display labels in key-value format
+- [ ] 3.7 Update describe commands for switches, routers, firewalls, systems, accesspoints, ups, desktops, laptops, services to display labels
+
+## 4. Web UI
+
+- [ ] 4.1 Create `ResourceLabelEditor` component in Shared.Rcl (mirror `ResourceTagEditor`, use `KeyValueModal`)
+- [ ] 4.2 Add `ResourceLabelEditor` to ServerCardComponent
+- [ ] 4.3 Add `ResourceLabelEditor` to SwitchCardComponent, RouterCardComponent, FirewallCardComponent
+- [ ] 4.4 Add `ResourceLabelEditor` to SystemCardComponent, AccessPointCardComponent, UpsCardComponent
+- [ ] 4.5 Add `ResourceLabelEditor` to DesktopCardComponent, LaptopCardComponent, ServiceCardComponent
+
+## 5. Unit Tests
+
+- [ ] 5.1 Add `AddLabelUseCaseTests` (new label, overwrite existing key, nonexistent resource)
+- [ ] 5.2 Add `RemoveLabelUseCaseTests` (remove existing, remove nonexistent key no-op)
+- [ ] 5.3 Add validation tests for `ThrowIfInvalid.LabelKey` and `ThrowIfInvalid.LabelValue`
+
+## 6. E2E Tests
+
+- [ ] 6.1 Add CLI workflow test: add label, verify YAML, describe shows labels, remove label
+- [ ] 6.2 Add browser E2E test for label add/remove in resource card