docs: add architecture decision records
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
# ADR 001: React Context for Centralized Rook-Ceph State
|
||||||
|
|
||||||
|
**Status**: Accepted
|
||||||
|
|
||||||
|
**Date**: 2026-03-05
|
||||||
|
|
||||||
|
**Deciders**: Development Team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The Rook-Ceph plugin needs to fetch and share data from many sources:
|
||||||
|
|
||||||
|
- **4 Ceph CRDs** under `ceph.rook.io/v1`: CephCluster, CephBlockPool, CephFilesystem, CephObjectStore
|
||||||
|
- **Standard K8s resources**: StorageClasses, PersistentVolumes, PersistentVolumeClaims
|
||||||
|
- **6 pod label selectors**: operator, mon, osd, mgr, CSI RBD, CSI CephFS
|
||||||
|
|
||||||
|
This data is consumed by 7+ page views and 3 detail view sections. The context exposes 16+ fields.
|
||||||
|
|
||||||
|
Data fetching uses a two-track strategy:
|
||||||
|
|
||||||
|
1. **Headlamp's `K8s.ResourceClasses.*.useList()`** for standard resources (StorageClasses, PVs, PVCs)
|
||||||
|
2. **`ApiProxy.request()` in `useEffect`** for CRDs and pods
|
||||||
|
|
||||||
|
Each API call is wrapped in its own `try/catch` for independent failure isolation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Use a single `RookCephDataProvider` React Context that centralizes all data fetching.
|
||||||
|
|
||||||
|
- Standard K8s resources use Headlamp's reactive `useList()` hooks.
|
||||||
|
- CRDs and pods use `ApiProxy.request()` in a single `useEffect` keyed on `refreshKey`.
|
||||||
|
- Expose all data, loading, error, and refresh via context value.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- ✅ Single fetch point avoids duplicate API calls across 7+ views
|
||||||
|
- ✅ All views share consistent data snapshot
|
||||||
|
- ✅ Error isolation per API call prevents one failure from blocking others
|
||||||
|
- ✅ Refresh mechanism updates everything atomically via `refreshKey`
|
||||||
|
- ⚠️ Large context (16+ fields) causes all consumers to re-render on any update
|
||||||
|
- ⚠️ Monolithic provider is complex to maintain
|
||||||
|
|
||||||
|
Mitigated by infrequent update cadence — data changes only on cluster state changes, not on user interaction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Individual hooks per resource type** — Rejected. Would cause duplicate fetches across 7 pages, each independently calling the same APIs.
|
||||||
|
|
||||||
|
2. **Multiple specialized contexts** (CephContext, StorageContext, PodContext) — Rejected. Adds provider nesting complexity, and the data is cross-referenced (e.g., PVC filtering depends on PV data).
|
||||||
|
|
||||||
|
3. **Redux / Zustand** — Rejected. Not available as a plugin dependency; Headlamp does not expose external state management libraries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- 2026-03-05: Initial decision accepted
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# ADR 002: extractJsonData() Pattern for KubeObject Unwrapping
|
||||||
|
|
||||||
|
**Status**: Accepted
|
||||||
|
|
||||||
|
**Date**: 2026-03-05
|
||||||
|
|
||||||
|
**Deciders**: Development Team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Headlamp's `useList()` hooks return arrays of `KubeObject` class instances that wrap raw JSON under `.jsonData`. The plugin's type system defines plain TypeScript interfaces (e.g., `CephCluster`, `StorageClass`) matching the raw Kubernetes JSON structure.
|
||||||
|
|
||||||
|
To use these typed interfaces, the `KubeObject` wrapper must be unwrapped. This pattern appears in every plugin that uses `useList()` hooks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Implement an `extractJsonData()` utility function that takes a `KubeObject` instance and returns the unwrapped `.jsonData` property.
|
||||||
|
|
||||||
|
- Apply this consistently to all `useList()` results before storing in context state.
|
||||||
|
- All type guards (e.g., `isRookCephProvisioner()`, `isRookCephStorageClass()`) operate on the unwrapped plain objects, not on `KubeObject` wrappers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- ✅ Clean separation between Headlamp's class instances and the plugin's typed interfaces
|
||||||
|
- ✅ Type guards work on plain objects, which are easier to test
|
||||||
|
- ✅ Consistent unwrapping pattern across all resources
|
||||||
|
- ⚠️ Extra mapping step on every `useList()` result
|
||||||
|
- ⚠️ Runtime cost of mapping arrays (negligible for typical cluster sizes of tens to hundreds of resources)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Use `KubeObject` instances directly** — Rejected. Type guards and filters become harder to write and test with class wrappers.
|
||||||
|
|
||||||
|
2. **Type assertion (`as CephCluster`)** — Rejected. Unsafe with no runtime validation; silently masks shape mismatches.
|
||||||
|
|
||||||
|
3. **Custom hook wrapping `useList()` with auto-extraction** — Considered but `extractJsonData()` is simpler and more explicit. A wrapper hook would hide the unwrapping step, making the data flow less obvious.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- 2026-03-05: Initial decision accepted
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# ADR 003: Strictly CommonComponents Only (No Direct MUI)
|
||||||
|
|
||||||
|
**Status**: Accepted
|
||||||
|
|
||||||
|
**Date**: 2026-03-05
|
||||||
|
|
||||||
|
**Deciders**: Development Team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Headlamp exports UI primitives through `@kinvolk/headlamp-plugin/lib/CommonComponents`:
|
||||||
|
|
||||||
|
- `SectionBox`, `SimpleTable`, `StatusLabel`, `NameValueTable`, and others
|
||||||
|
|
||||||
|
Headlamp also bundles MUI (`@mui/material`) as a shared external, making it technically accessible to plugins. Some plugins (e.g., polaris, sealed-secrets) directly use MUI components such as `Drawer`, `Alert`, and `useTheme`.
|
||||||
|
|
||||||
|
The Rook plugin must decide whether to use CommonComponents exclusively or mix in direct MUI usage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Use CommonComponents exclusively. No direct imports from `@mui/material`.
|
||||||
|
|
||||||
|
- All tables use `SimpleTable`
|
||||||
|
- All layout uses `SectionBox`
|
||||||
|
- All status indicators use `StatusLabel`
|
||||||
|
|
||||||
|
This creates a hard dependency only on Headlamp's public component API, not on MUI internals.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- ✅ Insulated from MUI version changes in Headlamp (e.g., MUI v5 to v6 migration)
|
||||||
|
- ✅ Consistent look-and-feel guaranteed by Headlamp's own components
|
||||||
|
- ✅ Simpler imports with a smaller effective API surface to learn
|
||||||
|
- ⚠️ Limited UI expressiveness — cannot use MUI `Drawer`, `Dialog`, `Stepper`, or other components not exposed by CommonComponents
|
||||||
|
- ⚠️ Some layouts require workarounds when CommonComponents lack needed primitives
|
||||||
|
|
||||||
|
Mitigated by the plugin's read-only nature, which reduces the need for complex interactive UI patterns (modals, steppers, drawers).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Mix CommonComponents with direct MUI** — Rejected for this plugin. Adds coupling risk to MUI internals, and the read-only UI does not need advanced MUI components.
|
||||||
|
|
||||||
|
2. **Use only MUI directly (skip CommonComponents)** — Rejected. Would miss Headlamp's styled wrappers and risk visual inconsistency with the rest of the Headlamp UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- 2026-03-05: Initial decision accepted
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# ADR 004: Read-Only Plugin with Cluster-Wide RBAC Scope
|
||||||
|
|
||||||
|
**Status**: Accepted
|
||||||
|
|
||||||
|
**Date**: 2026-03-05
|
||||||
|
|
||||||
|
**Deciders**: Development Team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Rook-Ceph manages cluster-wide storage infrastructure. The plugin needs to display:
|
||||||
|
|
||||||
|
- **Ceph CRDs**: CephClusters, CephBlockPools, CephFilesystems, CephObjectStores (all cluster-scoped or in the `rook-ceph` namespace)
|
||||||
|
- **Cluster-scoped K8s resources**: StorageClasses, PersistentVolumes
|
||||||
|
- **Namespace-spanning resources**: PersistentVolumeClaims (all namespaces)
|
||||||
|
|
||||||
|
The plugin could offer write operations (create/delete storage classes, manage pools) or remain strictly read-only. RBAC must cover all namespaces for PVCs to show complete storage utilization.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The plugin is strictly read-only — no create, update, delete, or patch operations.
|
||||||
|
|
||||||
|
- RBAC requires only `get` and `list` verbs across cluster scope.
|
||||||
|
- PVCs are fetched with `{namespace: ''}` (all namespaces).
|
||||||
|
- This minimizes the RBAC footprint while providing comprehensive visibility.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- ✅ Minimal RBAC requirements (read-only `get` and `list` only)
|
||||||
|
- ✅ No risk of accidental mutation of storage infrastructure
|
||||||
|
- ✅ Safe for monitoring and observability use cases
|
||||||
|
- ✅ Can be deployed in restrictive environments with minimal permissions
|
||||||
|
- ⚠️ Users cannot manage Rook resources from the UI
|
||||||
|
- ⚠️ Must use `kubectl` or the Rook toolbox for operational tasks
|
||||||
|
|
||||||
|
Mitigated by the plugin's purpose being observability, not management. Storage infrastructure changes are high-risk and better suited to GitOps or controlled `kubectl` workflows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Full CRUD operations** — Rejected. Storage infrastructure changes are high-risk and better suited to GitOps/kubectl workflows with proper review processes.
|
||||||
|
|
||||||
|
2. **Read-only with namespace-scoped PVC filtering** — Rejected. Would miss cross-namespace storage utilization data, providing an incomplete picture of cluster storage usage.
|
||||||
|
|
||||||
|
3. **Optional write mode via RBAC detection** — Rejected. Adds significant complexity (capability detection, conditional UI) for unclear benefit given the observability focus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- 2026-03-05: Initial decision accepted
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Architecture Decision Records (ADRs)
|
||||||
|
|
||||||
|
## What is an ADR?
|
||||||
|
|
||||||
|
An Architecture Decision Record (ADR) captures an important architectural decision made along with its context and consequences. ADRs are immutable once accepted — if a decision is reversed, a new ADR is created that supersedes the original.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
This project uses the [Nygard-style ADR format](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions):
|
||||||
|
|
||||||
|
- **Title**: Short noun phrase describing the decision
|
||||||
|
- **Status**: Proposed, Accepted, Deprecated, or Superseded
|
||||||
|
- **Context**: Forces at play, including technical, political, and project-specific
|
||||||
|
- **Decision**: The change being proposed or enacted
|
||||||
|
- **Consequences**: What becomes easier or harder as a result
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
| ADR | Title | Status | Date |
|
||||||
|
|-----|-------|--------|------|
|
||||||
|
| [001](001-react-context-state.md) | React Context for Centralized Rook-Ceph State | Accepted | 2026-03-05 |
|
||||||
|
| [002](002-extract-json-data.md) | extractJsonData() Pattern for KubeObject Unwrapping | Accepted | 2026-03-05 |
|
||||||
|
| [003](003-common-components-only.md) | Strictly CommonComponents Only (No Direct MUI) | Accepted | 2026-03-05 |
|
||||||
|
| [004](004-read-only-cluster-scope.md) | Read-Only Plugin with Cluster-Wide RBAC Scope | Accepted | 2026-03-05 |
|
||||||
|
|
||||||
|
## Creating New ADRs
|
||||||
|
|
||||||
|
1. Copy an existing ADR as a template.
|
||||||
|
2. Assign the next sequential number (e.g., `005`).
|
||||||
|
3. Fill in all sections: Context, Decision, Consequences, Alternatives Considered.
|
||||||
|
4. Set the status to **Proposed** and submit a PR for review.
|
||||||
|
5. Once merged, update the status to **Accepted** and add the entry to the index table above.
|
||||||
|
|
||||||
|
Use the filename pattern `NNN-short-slug.md` (e.g., `005-new-decision.md`).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Michael Nygard — Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions)
|
||||||
|
- [ADR GitHub Organization](https://adr.github.io/)
|
||||||
Reference in New Issue
Block a user