diff --git a/docs/architecture/adr/001-react-context-state.md b/docs/architecture/adr/001-react-context-state.md new file mode 100644 index 0000000..c495589 --- /dev/null +++ b/docs/architecture/adr/001-react-context-state.md @@ -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 diff --git a/docs/architecture/adr/002-extract-json-data.md b/docs/architecture/adr/002-extract-json-data.md new file mode 100644 index 0000000..dfa7b53 --- /dev/null +++ b/docs/architecture/adr/002-extract-json-data.md @@ -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 diff --git a/docs/architecture/adr/003-common-components-only.md b/docs/architecture/adr/003-common-components-only.md new file mode 100644 index 0000000..2d99c35 --- /dev/null +++ b/docs/architecture/adr/003-common-components-only.md @@ -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 diff --git a/docs/architecture/adr/004-read-only-cluster-scope.md b/docs/architecture/adr/004-read-only-cluster-scope.md new file mode 100644 index 0000000..45805da --- /dev/null +++ b/docs/architecture/adr/004-read-only-cluster-scope.md @@ -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 diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md new file mode 100644 index 0000000..fea8c02 --- /dev/null +++ b/docs/architecture/adr/README.md @@ -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/)