mirror of
https://github.com/vee1e/krply.git
synced 2026-09-01 09:49:51 +00:00
docs: add design and architecture documentation
This commit is contained in:
parent
22ba43a7ee
commit
b04fd7a406
13 changed files with 711 additions and 0 deletions
110
CONTRACT.md
Normal file
110
CONTRACT.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Cross-package contracts
|
||||
|
||||
This file pins the exported signatures that parallel development builds against. Do not change a contract without updating this file and every consumer. Internal implementations may change freely.
|
||||
|
||||
## Module
|
||||
|
||||
The module is github.com/krply/krply. It uses Go 1.26. It has no cgo. The pre-loaded dependencies are modernc.org/sqlite, k8s.io/api, k8s.io/apimachinery, k8s.io/client-go, sigs.k8s.io/yaml, github.com/spf13/cobra, github.com/prometheus/client_golang, and github.com/google/uuid. Do not add new dependencies without coordinating.
|
||||
|
||||
## internal/event (DONE)
|
||||
|
||||
- type Record. An immutable journal entry with the fields Type, EventID, IngestSeq, ObservedAt, WatchType, Synthetic, Resource (a ResourceRef), ObjectHash, Object (a json.RawMessage), Provenance, Gap, Coverage, Checkpoint, and Snapshot.
|
||||
- type RecordType. The constants are TypeEvent, TypeBaseline, TypeGap, TypeCoverageChange, TypeAuditCorrelation, TypeSnapshot, and TypeCheckpoint.
|
||||
- type WatchType. The constants are WatchAdded, WatchModified, WatchDeleted, WatchBookmark, and WatchError.
|
||||
- type ResourceRef. It has the methods ObjectKey() and GVK(), and the fields Group, Version, Kind, Namespace, Name, UID, and ResourceVersion.
|
||||
- type Stream with the fields ClusterID, Group, Version, Resource, Namespace, and Selector, and the method ID().
|
||||
- StreamID(string) (Stream, error).
|
||||
- EventID(stream, resource, watchType, observedAt) string. It is the deterministic dedup key.
|
||||
- ObjectHash([]byte) string.
|
||||
|
||||
## internal/storage (interface DONE, implementation pending)
|
||||
|
||||
- type Store interface. The methods are Append, Appends, ListClusters, Streams, StreamMeta, Events(EventFilter), ObjectHistory(ObjectRef), ObjectAt, StreamEvents, Baselines, Gaps, SaveSnapshot, Snapshots, and Close.
|
||||
- The package also exports StreamMeta, EventFilter, ObjectRef, and SnapshotRef.
|
||||
- The constructors are NewSQLiteStore(path string) (Store, error) and NewInMemory() Store. Both must exist.
|
||||
- SQLite must use WAL mode. It must write records and the checkpoint atomically.
|
||||
- Implementations return events in ingest order. The field IngestSeq ascends.
|
||||
|
||||
## internal/discovery (implementation)
|
||||
|
||||
- type ResourceSpec with the fields APIGroup, Version, Resource, Kind, and Namespace.
|
||||
- Discover(ctx, client) resolves well-known aliases such as deploy, sts, ds, and cm to full specs. It fills missing versions and kinds through ServerResources. An empty Namespace means cluster scope.
|
||||
- DefaultResources() returns the MVP watch list. The list is namespaces, configmaps, services, deployments, statefulsets, daemonsets, runtimeclasses, and pods.
|
||||
|
||||
## internal/watch (implementation)
|
||||
|
||||
- type Config with the fields KubeConfig, Context, ClusterID, Resources (a slice of discovery.ResourceSpec), Selector, Store (a storage.Store), Log (a *slog.Logger), Bookmarks, SendInitial, AgentName, MinBackoff, and MaxBackoff.
|
||||
- NewCollector(cfg Config) (*Collector, error).
|
||||
- Run(ctx) error runs all streams.
|
||||
- ClusterID(ctx, kubeconfigPath, context) derives a stable cluster identity from the kubeconfig. It hashes the server URL.
|
||||
- Watch semantics: start from the exact resource version. On a close, reconnect from the last durable resource version. On a 410 Gone response, write a GAP record, relist, and write a BASELINE. Write every raw event through store.Append. Bookmarks advance checkpoints only, as TypeCheckpoint. Send an initial BASELINE from the first list. Mark events in the initial list as Synthetic.
|
||||
|
||||
## internal/materialize (implementation)
|
||||
|
||||
- type ObjectState with the fields ClusterID, StreamID, Namespace, Name, Kind, Object (a json.RawMessage), and At (a time.Time).
|
||||
- type Snapshot with the fields ID, ClusterID, Name, At, Objects, Streams, Complete, Missing, and Warning.
|
||||
- type Materializer. The constructor is NewMaterializer(store storage.Store).
|
||||
- ObjectAt(ctx, clusterID, streamID, namespace, name, at) (*event.Record, error).
|
||||
- Snapshot(ctx, clusterID, at, name) (*Snapshot, error). It materializes every watched object, records per-stream watermarks, stores a TypeSnapshot record, and marks incomplete streams.
|
||||
- Diff(ctx, clusterID, namespace, before, after) (*DiffResult, error). DiffResult has the fields Before, After, Changes, HasGaps, and Warning. ObjectDiff has the fields Namespace, Name, Kind, and Changes. FieldChange has the fields Path, Before, After, Added, and Removed.
|
||||
- The field diff must ignore managedFields, resourceVersion, uid, generation, creationTimestamp, and other server metadata.
|
||||
|
||||
## internal/replay (implementation)
|
||||
|
||||
- type Policy with the allowlist fields IncludeSecrets, IncludeRoles, IncludePods, IncludeJobs, IncludePV, IncludeWebhooks, IncludeCRDs, AllowFinalizers, and MapNamespaces.
|
||||
- type PlanObject with the fields Namespace, Name, Kind, Order, Object (a map[string]any), and Warnings.
|
||||
- type Excluded with the fields Namespace, Name, Kind, and Reason.
|
||||
- type Plan with the fields ID, ClusterID, SnapshotID, SourceNamespace, TargetNamespace, TargetContext, FieldManager, Objects, Warnings, Excluded, CoverageComplete, and Status.
|
||||
- type Planner. The constructor is NewPlanner(store, mat, policy).
|
||||
- Plan(ctx, clusterID, snapshotID, sourceNS, targetNS) (*Plan, error).
|
||||
- Sanitization defaults follow docs/replay-safety/replay-safety.md. Remove uid, resourceVersion, creationTimestamp, generation, managedFields, deletionTimestamp, status, ownerReferences (except approved), and finalizers (unless allowlisted). Remove Service clusterIP fields. Exclude Secrets, RBAC, Pods, Jobs, PVs, webhooks, and CRDs by default.
|
||||
- DryRun(ctx, kubeconfig, context) (*DryRunResult, error).
|
||||
- Apply(ctx, kubeconfig, context, confirm) (*ApplyResult, error).
|
||||
- type DryRunResult with the fields Applied, Conflicts, Errors, and OK.
|
||||
- type ApplyResult with the fields PlanID, Applied, and Errors.
|
||||
- type DryRunItem with the fields Namespace, Name, Kind, Manager, and Message.
|
||||
- Use SSA with the field manager krply-plan-<planID>. Never force. Run the dry run first.
|
||||
- Plans refuse incomplete coverage unless the Policy field AllowGaps is true.
|
||||
|
||||
## internal/audit (implementation)
|
||||
|
||||
- type AuditEvent with the fields ClusterID, RequestID, Verb, Resource, Namespace, Name, UID, ResourceVersion, User, UserAgent, SourceIPs, ResponseCode, Stage, Object, ResponseObject, Annotations, and Timestamp.
|
||||
- type Correlator. The constructor is NewCorrelator(store).
|
||||
- Ingest(ctx, events) error. It matches stored events best-effort by cluster, namespace, name, uid, and resourceVersion. It appends TypeAuditCorrelation records.
|
||||
- Match(ctx, evt) (string, error). It returns the event_id or ErrNoMatch. Export var ErrNoMatch = errors.New(...).
|
||||
|
||||
## internal/metrics (implementation)
|
||||
|
||||
- Expose Prometheus metrics for ingest counts, gaps, degraded streams, and storage bytes. NewRegistry(store) returns the registry.
|
||||
|
||||
## internal/api (implementation)
|
||||
|
||||
- NewServer(store, mat, plan, reg) (*Server, error).
|
||||
- Handler() http.Handler. The routes are GET /v1/health, /v1/clusters, /v1/streams, /v1/events, /v1/objects/{ref}/history, /v1/diff, /v1/snapshots, POST and GET /v1/replay-plans, POST /v1/replay-runs, and /metrics.
|
||||
- Use the JSON types in api/query/v1. Every historical query returns gaps and warnings.
|
||||
|
||||
## cmd/krply-server
|
||||
|
||||
- The main function builds the store, materializer, planner, and api server. It serves on the address from the listen flag, default :8080. Flags: store PATH, listen ADDR, and version.
|
||||
|
||||
## cmd/krply (CLI, cobra)
|
||||
|
||||
- Commands: record, status, coverage, timeline, diff, snapshot, reconstruct, replay plan, replay apply, and export.
|
||||
- Flags include context, namespace, resource, store, since, target-context, plan-id, confirm, and others.
|
||||
- The record command uses the internal/watch Collector. All other commands read the local store directly. No server is required. The server URL flag switches to the HTTP client.
|
||||
|
||||
## web/
|
||||
|
||||
- A vanilla static UI lives in web/src. krply-server serves it at the root path. Views: coverage matrix, stream health, object timeline, JSON diff, gap markers, and replay review. The npm run build command outputs to web/dist. No framework dependency is required. A lightweight build such as esbuild or vite is acceptable if the config is committed.
|
||||
|
||||
## deploy/
|
||||
|
||||
- The deploy/rbac directory has the recorder ClusterRole with get, list, and watch on specific resources and no secrets, the replay ClusterRole with create and patch only, and a kustomization.
|
||||
- The deploy/compose/docker-compose.yml file runs krply-server and an optional agent.
|
||||
- The deploy/helm/krply directory has a minimal chart with values.yaml, Chart.yaml, and templates.
|
||||
|
||||
## tests
|
||||
|
||||
- test/unit has small Go tests that exercise the exported packages. You may run go test on the packages directly instead.
|
||||
- test/integration has the tag integration. It uses a fake apiserver, k8s.io/apimachinery, and httptest. It covers 410 relist, bookmark handling, disconnects, exact-RV watch start, and RBAC denial.
|
||||
- test/e2e has the tag e2e. It runs the full record, timeline, snapshot, replay-plan flow against a fake apiserver. No real cluster is needed.
|
||||
161
README.md
Normal file
161
README.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# krply
|
||||
|
||||
Gap-aware Kubernetes object history and replay planning.
|
||||
|
||||
krply records selected Kubernetes watch events in a local SQLite journal. It
|
||||
shows timelines, field changes, coverage, snapshots, and safe replay plans.
|
||||
|
||||
## Features
|
||||
|
||||
- List and watch selected resources.
|
||||
- Resume from durable resource versions.
|
||||
- Record bookmarks as progress checkpoints.
|
||||
- Mark 410 Gone responses as visible gaps.
|
||||
- Reconstruct object state at a time.
|
||||
- Compare state before and after a time.
|
||||
- Build sanitized server-side apply plans.
|
||||
- Review data through a CLI, HTTP API, or web UI.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
K["Kubernetes API"] --> C["Collector"]
|
||||
C --> J["SQLite journal"]
|
||||
J --> M["Materializer"]
|
||||
M --> Q["Query API"]
|
||||
Q --> CLI["CLI"]
|
||||
Q --> UI["Web UI"]
|
||||
M --> P["Replay planner"]
|
||||
P --> T["Test cluster"]
|
||||
```
|
||||
|
||||
The collector writes the raw event before it advances the checkpoint. A
|
||||
reconnect can deliver an event again. The journal deduplicates that event.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Go 1.26 or newer.
|
||||
- kubectl and access to a Kubernetes cluster for recording.
|
||||
- A kubeconfig with get, list, and watch access to selected resources.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
make build
|
||||
```
|
||||
|
||||
Binaries are written to `bin/krply` and `bin/krply-server`.
|
||||
|
||||
## Quick start
|
||||
|
||||
Record selected resources:
|
||||
|
||||
```sh
|
||||
./bin/krply record \
|
||||
--kubeconfig ~/.kube/config \
|
||||
--context prod \
|
||||
--namespace shop \
|
||||
--resource deployments \
|
||||
--resource configmaps \
|
||||
--resource services \
|
||||
--store ./krply.db \
|
||||
--bookmarks
|
||||
```
|
||||
|
||||
Inspect the journal:
|
||||
|
||||
```sh
|
||||
./bin/krply status --store ./krply.db
|
||||
./bin/krply coverage --store ./krply.db
|
||||
./bin/krply timeline checkout-service --namespace shop --kind Deployment --store ./krply.db
|
||||
./bin/krply diff --since 30m --until now --namespace shop --store ./krply.db
|
||||
./bin/krply snapshot --store ./krply.db
|
||||
```
|
||||
|
||||
Start the web UI and API:
|
||||
|
||||
```sh
|
||||
./bin/krply-server --store ./krply.db --listen :8080
|
||||
```
|
||||
|
||||
Open http://localhost:8080.
|
||||
|
||||
## Live demo
|
||||
|
||||
The following output came from a live kind cluster. The recording had four
|
||||
streams and zero gaps.
|
||||
|
||||
```text
|
||||
$ krply coverage --store /tmp/krply-live.db
|
||||
STREAM RESOURCE NAMESPACE AVAIL LAST-RV GAPS COVERAGE
|
||||
cluster-2b6b99a0-kind-krply-demo//v1/configmaps/shop/ configmaps shop true 2005 0 OK
|
||||
cluster-2b6b99a0-kind-krply-demo//v1/services/shop/ services shop true 2023 0 OK
|
||||
cluster-2b6b99a0-kind-krply-demo/apps/v1/deployments/shop/ apps/deployments shop true 1994 0 OK
|
||||
cluster-2b6b99a0-kind-krply-demo/apps/v1/statefulsets/shop/ apps/statefulsets shop true 653 0 OK
|
||||
```
|
||||
|
||||
The real diff from that recording:
|
||||
|
||||
```text
|
||||
$ krply diff --since 2026-08-05T23:41:40Z --until now --namespace shop --store /tmp/krply-live.db
|
||||
CHANGED 3 objects
|
||||
ConfigMap shop/app-config
|
||||
data.log_level info -> debug
|
||||
Deployment shop/checkout-service
|
||||
spec.replicas 2 -> 5
|
||||
spec.template.spec.containers[0].image nginx:1.25 -> nginx:1.27
|
||||
Service shop/checkout-service
|
||||
metadata.labels.team null -> payments
|
||||
```
|
||||
|
||||
## Web UI
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Replay safety
|
||||
|
||||
`replay plan` is the normal entry point. It reconstructs state, checks
|
||||
coverage, removes server-owned fields, maps namespaces, sorts resources, and
|
||||
runs a server-side dry run.
|
||||
|
||||
It excludes Secrets, RBAC objects, Pods, Jobs, persistent storage, webhooks,
|
||||
and CRDs by default. It never forces server-side apply conflicts. Apply
|
||||
requires an explicit confirmation.
|
||||
|
||||
## Consistency
|
||||
|
||||
- Resource versions are comparable only within one cluster and API resource.
|
||||
- Ordering is guaranteed only within one watch stream.
|
||||
- `observed_at` is collector observation time, not object change time.
|
||||
- A snapshot is complete only when every contributing stream has a baseline and
|
||||
no gap.
|
||||
- Historical queries always return coverage information.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Architecture](docs/architecture/architecture.md)
|
||||
- [Consistency model](docs/consistency/consistency.md)
|
||||
- [Event schema](docs/event-schema/event-schema.md)
|
||||
- [Replay safety](docs/replay-safety/replay-safety.md)
|
||||
- [Threat model](docs/threat-model/threat-model.md)
|
||||
- [Deployment manifests](deploy/)
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
make build
|
||||
make test
|
||||
make test-integration
|
||||
make test-e2e
|
||||
make web
|
||||
```
|
||||
|
||||
The repository contains unit tests, a fake Kubernetes API server, and an
|
||||
end-to-end recording pipeline. No real cluster is required for the tests.
|
||||
102
docs/architecture/architecture.md
Normal file
102
docs/architecture/architecture.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Architecture
|
||||
|
||||
krply is a gap-aware Kubernetes object history and replay planner. It records watch events for a small, explicit allowlist of resources. It stores them in a durable local journal. It materializes object state on demand. It plans safe replays into a disposable cluster. See README.md for the product summary and product boundary.
|
||||
|
||||
## Component diagram
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
K8s["Kubernetes API"] -->|"list / watch (get/list/watch)"| Col["Collector"]
|
||||
Col -->|raw events| JW["Journal writer"]
|
||||
JW -->|"event + checkpoint (same transaction)"| GD["Gap detector"]
|
||||
JW -->|records| Store["Event store (SQLite WAL)"]
|
||||
GD -->|"coverage + gaps"| Store
|
||||
Audit["Audit log sink"] -.->|"provenance, optional"| Store
|
||||
Store --> Mat["Materializer"]
|
||||
Mat -->|"events -> object state"| QA["Query API (HTTP /v1)"]
|
||||
Mat -->|snapshots, diffs| RP["Replay planner"]
|
||||
QA --> CLI["CLI"]
|
||||
QA --> UI["Web UI"]
|
||||
RP -->|"sanitize -> plan -> dry run -> apply"| Sandbox["Sandbox cluster"]
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Role | Reads | Writes |
|
||||
|---|---|---|---|
|
||||
| Collector | Discovers resources, lists, watches, reconnects, relists | Kubernetes API | Journal |
|
||||
| Journal writer | Persists raw events and checkpoints durably in one transaction | Collector | Event store |
|
||||
| Gap detector | Marks continuity loss and coverage changes (GAP, COVERAGE_CHANGE) | Journal | Event store |
|
||||
| Event store | Holds raw events, indexes, baselines, snapshots, coverage | Journal writer, gap detector | none |
|
||||
| Materializer | Reduces events into object state; builds snapshots and diffs | Event store | SNAPSHOT records |
|
||||
| Query API | Serves timelines, diffs, snapshots, and plans over HTTP | Event store, materializer | Replay plans |
|
||||
| Replay planner | Sanitizes state and plans a safe apply into a sandbox | Event store, materializer | Plans, dry-run results |
|
||||
| CLI and UI | Interfaces for investigation and replay review | Query API or local store | none |
|
||||
|
||||
## Collector lifecycle
|
||||
|
||||
For each stream the collector does the following steps. It discovers the resource and its scope. It lists the collection and records the collection resource version. It persists a BASELINE record. It watches from that exact resource version.
|
||||
|
||||
Then, per event:
|
||||
|
||||
- ADDED, MODIFIED, and DELETED persist the raw event and advance the checkpoint in the same transaction.
|
||||
- BOOKMARK persists a progress checkpoint only. It never becomes an object change.
|
||||
- A stream close reconnects from the last durable resource version.
|
||||
- A 410 Gone response writes a GAP record, relists, writes a new BASELINE, and watches again.
|
||||
|
||||
A relist restores current state. It cannot reveal what changed during the gap. Every event in the initial list is marked synthetic. It is a baseline snapshot, not a creation event.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Discover resource and scope"] --> B["List collection"]
|
||||
B --> C["Persist BASELINE record"]
|
||||
C --> D["Watch from exact resource version"]
|
||||
D --> E{"Event type"}
|
||||
E -->|"ADDED, MODIFIED, DELETED"| F["Persist raw event and checkpoint"]
|
||||
F --> D
|
||||
E -->|BOOKMARK| G["Persist progress checkpoint only"]
|
||||
G --> D
|
||||
D --> H{"Connection closed?"}
|
||||
H -->|yes| I["Reconnect from last durable resource version"]
|
||||
I --> D
|
||||
D --> J{"410 Gone?"}
|
||||
J -->|yes| K["Persist GAP record"]
|
||||
K --> L["Relist"]
|
||||
L --> M["Persist new BASELINE"]
|
||||
M --> D
|
||||
```
|
||||
|
||||
## Storage layout
|
||||
|
||||
The MVP store is SQLite in WAL mode. It has one writer and it works on one host only. Records and checkpoints are written atomically.
|
||||
|
||||
The planned object-storage segment layout for the central multi-cluster service is:
|
||||
|
||||
- cluster/date/resource/segment-0001.ndjson.zst
|
||||
- cluster/date/resource/segment-0001.manifest.json
|
||||
|
||||
Each manifest records the event count, the sequence range, the per-stream resource versions, a checksum, the redaction policy version, and the schema version.
|
||||
|
||||
## API endpoints
|
||||
|
||||
Historical queries always return coverage and gap information. A partial result carries a warning.
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
|---|---|---|
|
||||
| GET | /v1/clusters | List cluster identities |
|
||||
| GET | /v1/streams | Show streams and coverage |
|
||||
| GET | /v1/events | Query events with cursor pagination |
|
||||
| GET | /v1/objects/{ref}/history | One object history |
|
||||
| GET | /v1/diff | Compare two time boundaries |
|
||||
| GET | /v1/snapshots | List materialized snapshots |
|
||||
| POST | /v1/replay-plans | Create a sanitized plan |
|
||||
| GET | /v1/replay-plans/{id} | Plan, warnings, dry-run result |
|
||||
| POST | /v1/replay-runs | Apply an approved plan |
|
||||
| GET | /v1/health | Service health |
|
||||
| GET | /metrics | Prometheus metrics |
|
||||
|
||||
The API serves the web UI at the root path. CLI commands read the local store directly. The server flag switches them to the HTTP client.
|
||||
|
||||
## Multi-cluster
|
||||
|
||||
Each cluster gets an identity with an immutable generation. Resource versions and UIDs are never compared or merged across clusters. A local agent per cluster is preferred over a central service that holds credentials for every cluster.
|
||||
94
docs/consistency/consistency.md
Normal file
94
docs/consistency/consistency.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Consistency
|
||||
|
||||
krply makes no claim that Kubernetes provides immutable history. It stores its own journal and always shows the truth about coverage. This document defines the consistency guarantees that consumers can rely on.
|
||||
|
||||
## Consistency model
|
||||
|
||||
| Area | Guarantee |
|
||||
|---|---|
|
||||
| Ordering | Ordered within one watch stream |
|
||||
| Resource version | Meaningful only within one cluster and API resource |
|
||||
| Ingestion | At least once with deduplication |
|
||||
| Historical state | Reconstructable only from a complete baseline plus complete events |
|
||||
| Cluster snapshot | A vector of per-stream watermarks, not one atomic snapshot |
|
||||
| Timestamps | observed_at means collector observation time |
|
||||
| Cross-cluster order | None |
|
||||
| Audit identity | Only through optional audit-log correlation |
|
||||
| Replay | Declarative state application, not exact side-effect reproduction |
|
||||
|
||||
Two consequences follow directly:
|
||||
|
||||
- There is no total order across resources. Two streams are ordered within themselves only. You cannot say that a Deployment change happened before a ConfigMap change unless they share a stream.
|
||||
- The field observed_at is when the collector observed the event, not when the object changed. The UI shows both the source resource version and the observed time. It never labels observed time as changed time.
|
||||
|
||||
## Watch semantics
|
||||
|
||||
### Resource versions
|
||||
|
||||
- Store and compare resource versions as opaque strings.
|
||||
- Compare them only within one cluster and one API resource. A Deployment resource version is not comparable with a Pod resource version.
|
||||
- A resource version is not a global cluster clock.
|
||||
- Extension API servers may use non-numeric resource versions. Never parse them as integers.
|
||||
|
||||
### The normal pattern
|
||||
|
||||
1. List a collection. Record the collection resource version.
|
||||
2. Watch from that exact resource version.
|
||||
3. Apply ADDED, MODIFIED, and DELETED to the journal.
|
||||
4. Reconnect from the last durable resource version on stream close.
|
||||
|
||||
### Bookmarks
|
||||
|
||||
- Bookmarks are progress markers, not object changes.
|
||||
- The API server may ignore the allowWatchBookmarks flag.
|
||||
- A bookmark may advance the collector durable checkpoint safely.
|
||||
- A bookmark must never appear as an object change in the UI or in diffs. Bookmark events persist as TypeCheckpoint records, not object events.
|
||||
|
||||
### Compaction and relists
|
||||
|
||||
- The API server retains watch history for a limited window. The default etcd history window is short. Its exact length depends on cluster configuration.
|
||||
- On a 410 Gone response: write a GAP record, relist, store a new BASELINE, and watch again.
|
||||
- A relist restores current state. It cannot reveal what changed during the gap. The gap remains visible in the journal and in query results.
|
||||
|
||||
### Streaming initial events
|
||||
|
||||
- The sendInitialEvents flag may send synthetic ADDED events for the existing collection.
|
||||
- These events are a baseline snapshot, not creation events. Mark them synthetic.
|
||||
- Keep the conventional list-plus-watch fallback.
|
||||
|
||||
## Ingestion guarantee
|
||||
|
||||
The journal write uses the write-event-then-checkpoint order:
|
||||
|
||||
1. Write the raw event first.
|
||||
2. Update the checkpoint in the same transaction.
|
||||
3. On restart, replay from the last checkpoint and deduplicate.
|
||||
|
||||
This gives at-least-once delivery with deterministic deduplication. If the process crashes between writing an event and its checkpoint, the event may be written again. The field event_id is a deterministic key derived from the stream, the resource, the watch type, and the observed time. This key makes re-application idempotent.
|
||||
|
||||
These behaviors are NOT guaranteed:
|
||||
|
||||
- Exactly-once network delivery. The API server can resend after a reconnect.
|
||||
- A crash-safe guarantee that an event exists in the journal if the checkpoint does not reference it. Events that are written but not checkpointed may be redelivered.
|
||||
|
||||
## Watermarks
|
||||
|
||||
A cluster snapshot is not one atomic snapshot. It is a vector of per-stream watermarks. Each stream reports the last resource version that the collector processed durably. A snapshot therefore has:
|
||||
|
||||
- Per-stream boundaries, called watermarks.
|
||||
- A completeness flag, called Complete.
|
||||
- A list of Missing streams when any stream is incomplete or has an unresolved gap.
|
||||
|
||||
Query results and snapshots expose gaps and warnings. A partial result carries a warning. Never present a partial stream as complete.
|
||||
|
||||
## Timestamps
|
||||
|
||||
- The field observed_at is the collector observation time. The collector clock sets it.
|
||||
- The source resource version is the server ordering token. The UI shows it alongside the observed time.
|
||||
- Diffs and snapshots use observed_at as the key. Clock differences between the collector and the API server affect when a change is attributed. They never affect which changes are recorded.
|
||||
|
||||
## See also
|
||||
|
||||
- Watch semantics source: Kubernetes API concepts.
|
||||
- Risks and their controls: ../threat-model/threat-model.md.
|
||||
- Storage layout: ../architecture/architecture.md.
|
||||
65
docs/event-schema/event-schema.md
Normal file
65
docs/event-schema/event-schema.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Event schema
|
||||
|
||||
Every journal record is an immutable entry. The wire format is api/event/v1. This document describes the fields, the record types, and how keys and hashes are derived.
|
||||
|
||||
## Record fields
|
||||
|
||||
| Field | Purpose |
|
||||
|---|---|
|
||||
| cluster_id | Separates resource version and UID domains; one per physical cluster |
|
||||
| stream_id | Identifies group, version, resource, namespace, and selector |
|
||||
| event_id | Deterministic deduplication key |
|
||||
| ingest_sequence | Local storage order (ascending); returned in this order |
|
||||
| observed_at | Collector observation time |
|
||||
| watch_type | Original event type (ADDED, MODIFIED, DELETED, BOOKMARK, ERROR) |
|
||||
| synthetic | True for baseline events |
|
||||
| resource | Group, version, kind, namespace, name, UID, resource version |
|
||||
| object_hash | Fast equality and deduplication |
|
||||
| object | Raw or faithfully preserved payload (JSON) |
|
||||
| provenance | Optional audit correlation |
|
||||
|
||||
The raw watch payload is kept separate from normalized fields. This preserves unknown CRD fields. Never re-serialize the object into a lossy normalized shape.
|
||||
|
||||
## Record types
|
||||
|
||||
The journal stores event records plus these special records:
|
||||
|
||||
| Type | Meaning |
|
||||
|---|---|
|
||||
| event | A watch event (ADDED, MODIFIED, DELETED) |
|
||||
| baseline | A list result (initial or post-relist) |
|
||||
| gap | Continuity was lost (for example, a 410 Gone response) |
|
||||
| coverage_change | A resource became unavailable or was newly discovered |
|
||||
| audit_correlation | Optional request provenance matched from audit logs |
|
||||
| snapshot | A materialized set of stream boundaries |
|
||||
| checkpoint | Progress marker advanced by a bookmark |
|
||||
|
||||
The corresponding record type constants in internal/event are: TypeEvent, TypeBaseline, TypeGap, TypeCoverageChange, TypeAuditCorrelation, TypeSnapshot, and TypeCheckpoint. The watch types are: WatchAdded, WatchModified, WatchDeleted, WatchBookmark, and WatchError.
|
||||
|
||||
## event_id derivation
|
||||
|
||||
The field event_id is the deterministic deduplication key. The derivation is:
|
||||
|
||||
```
|
||||
EventID(stream, resource, watchType, observedAt) -> string
|
||||
```
|
||||
|
||||
The inputs are the stream identity, the resource reference, the watch type, and the observed time. The same key is recomputed for a redelivered event, for example after a reconnect or a crash before the checkpoint. Re-application is therefore idempotent. This makes at-least-once ingestion safe. See ../consistency/consistency.md for the ingestion guarantee.
|
||||
|
||||
## object_hash
|
||||
|
||||
The function ObjectHash(objectJSON) returns a fast equality and deduplication hash over the raw object payload. It lets consumers detect that a MODIFIED event did not change the object. It lets the store collapse no-op writes. It is not a cryptographic commitment. Treat it as a performance and equality helper only.
|
||||
|
||||
## Provenance
|
||||
|
||||
The field provenance is optional. When audit logs are available, an audit event is correlated to a stored event by cluster, namespace, name, UID, and resource version. It is recorded as a TypeAuditCorrelation record that references the field event_id. Audit identity is available only through this optional correlation. A watch event itself never contains the actor who made the change.
|
||||
|
||||
## Versioning
|
||||
|
||||
The package api/event/v1 is the public wire format. Internal packages stay private until the schema and replay contract stabilize. The manifest used by the object-storage segment layout records a schema version alongside the redaction policy version. Do not change api/event/v1 without a coordinated schema bump. Consumers of the journal and replay contract depend on it.
|
||||
|
||||
## See also
|
||||
|
||||
- Ingestion and deduplication: ../consistency/consistency.md.
|
||||
- Storage layout and segment manifests: ../architecture/architecture.md.
|
||||
- Sanitization of the object payload before replay: ../replay-safety/replay-safety.md.
|
||||
BIN
docs/images/shot-coverage.png
Normal file
BIN
docs/images/shot-coverage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/images/shot-diff.png
Normal file
BIN
docs/images/shot-diff.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
BIN
docs/images/shot-plans.png
Normal file
BIN
docs/images/shot-plans.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
BIN
docs/images/shot-snapshots.png
Normal file
BIN
docs/images/shot-snapshots.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/images/shot-streams.png
Normal file
BIN
docs/images/shot-streams.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
BIN
docs/images/shot-timeline.png
Normal file
BIN
docs/images/shot-timeline.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 374 KiB |
105
docs/replay-safety/replay-safety.md
Normal file
105
docs/replay-safety/replay-safety.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Replay safety
|
||||
|
||||
Replay does NOT mean sending historical events back to a live cluster. krply reconstructs declarative state locally. It sanitizes the state. It plans an apply into a disposable target. It refuses to proceed without explicit approval. This document defines the replay safety rules.
|
||||
|
||||
## The 9-step flow
|
||||
|
||||
1. **Reconstruct**. Reconstruct the selected state locally from the journal.
|
||||
2. **Verify stream completeness**. Every stream that feeds the plan must be complete with no unresolved gap.
|
||||
3. **Remove server-generated fields**. Remove UIDs, resource versions, timestamps, managedFields, status, and other fields. The table below lists the defaults.
|
||||
4. **Map namespaces and names**. Map source namespaces and names to target values. A source UID is never carried over.
|
||||
5. **Sort resources by dependency**. Sort namespaces first, then declarative roots.
|
||||
6. **Run a server-side dry run** with the synthetic field manager.
|
||||
7. **Show the plan and warnings**. Include dry-run conflicts and errors.
|
||||
8. **Apply only after explicit approval**. The replay apply command requires a target context, a plan ID, a namespace allowlist, a successful dry run, and a confirmation flag.
|
||||
9. **Observe the target without assuming convergence**. The tool does not prove that the target reached the intended state. Target controllers may still change it.
|
||||
|
||||
## Sanitization defaults
|
||||
|
||||
| Source field | Default action |
|
||||
|---|---|
|
||||
| metadata.uid | Remove |
|
||||
| metadata.resourceVersion | Remove |
|
||||
| metadata.creationTimestamp | Remove |
|
||||
| metadata.generation | Remove |
|
||||
| metadata.managedFields | Remove |
|
||||
| metadata.deletionTimestamp | Remove |
|
||||
| status | Remove |
|
||||
| ownerReferences | Remove or remap (see below) |
|
||||
| finalizers | Remove unless allowlisted |
|
||||
| Service cluster IP fields | Reject or transform |
|
||||
| Secret data | Exclude by default |
|
||||
| RBAC objects | Exclude by default |
|
||||
|
||||
## Default exclusions
|
||||
|
||||
The planner excludes these kinds by default. The Policy object configures the exceptions:
|
||||
|
||||
- Secrets.
|
||||
- ServiceAccounts and token objects.
|
||||
- Roles, ClusterRoles, and bindings.
|
||||
- Jobs and CronJobs.
|
||||
- Pods.
|
||||
- PersistentVolumes and claims.
|
||||
- LoadBalancer Services.
|
||||
- Storage resources.
|
||||
- Admission webhooks.
|
||||
- CRDs.
|
||||
|
||||
## MVP replay roots
|
||||
|
||||
Only these kinds can be replayed in the MVP:
|
||||
|
||||
- Namespaces.
|
||||
- ConfigMaps, with sensitivity review. ConfigMaps can contain secrets.
|
||||
- Safe Services.
|
||||
- Deployments.
|
||||
- StatefulSets, with warnings.
|
||||
- DaemonSets.
|
||||
- RuntimeClasses, plan-only.
|
||||
|
||||
## SSA rules (section 12)
|
||||
|
||||
- Use a synthetic field manager derived from the plan, for example krply-plan-<planID>. Never reuse the original manager name.
|
||||
- No forced conflicts by default.
|
||||
- Run a server-side dry run first.
|
||||
- A conflict means the plan needs review. It is surfaced as a dry-run item, not silently resolved.
|
||||
- The field managedFields is server-managed metadata. It is never copied into replay.
|
||||
|
||||
## Owner references (section 11)
|
||||
|
||||
- Owner references connect a dependent to an owner. They influence garbage collection. They are NOT evidence of who changed an object.
|
||||
- A source UID is not valid in a target cluster.
|
||||
- Replay declarative root objects. Let target controllers create ReplicaSets and Pods.
|
||||
- Remove generated owner references by default.
|
||||
- Treat finalizers as dangerous. Remove them unless approved.
|
||||
|
||||
## When a plan is refused
|
||||
|
||||
The planner refuses to produce or apply a plan in these cases:
|
||||
|
||||
- **Coverage is incomplete**. Any stream that feeds the plan has an unresolved gap, unless the Policy flag AllowGaps is set explicitly.
|
||||
- **Dry run fails**. The server-side dry run reports conflicts or errors that are not overridden.
|
||||
- **No explicit approval**. The apply command is missing the target context, the plan ID, a namespace allowlist, a successful dry run, or the confirmation flag.
|
||||
- **Excluded kinds requested**. A request to replay an excluded kind, such as Secrets, Pods, RBAC, PVs, webhooks, or CRDs, is refused unless the corresponding Policy allowlist flag is set.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Historical state"] --> B["Materialize selected state"]
|
||||
B --> C{"Coverage complete?"}
|
||||
C -->|no| D["Refuse or require allow-gaps"]
|
||||
C -->|yes| E["Sanitize server fields"]
|
||||
E --> F["Map namespaces"]
|
||||
F --> G["Sort declarative roots"]
|
||||
G --> H["Server-side dry run"]
|
||||
H --> I{"Approved?"}
|
||||
I -->|no| J["Keep plan only"]
|
||||
I -->|yes| K["Apply with synthetic manager"]
|
||||
K --> L["Observe target controllers"]
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- RBAC for recorder and replay identities: ../../deploy/rbac/.
|
||||
- Threat model and the unsafe-replay control: ../threat-model/threat-model.md.
|
||||
- Consistency requirements behind step 2: ../consistency/consistency.md.
|
||||
74
docs/threat-model/threat-model.md
Normal file
74
docs/threat-model/threat-model.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Threat model
|
||||
|
||||
This document lists the threats that krply is designed against, the controls that mitigate them, the trust boundaries, and the RBAC model. The deployment RBAC manifests are in deploy/rbac/.
|
||||
|
||||
## Threats and controls
|
||||
|
||||
| Threat | Impact | Control | Where |
|
||||
|---|---|---|---|
|
||||
| Secret exposure | Credential leak | Exclude Secrets by default, redact ConfigMaps, encrypt the journal at rest if the host requires it | replay-safety, recorder RBAC |
|
||||
| Unsafe replay | Production damage | Plan first, dry run, allowlist, explicit confirm flag, required target context, no write on source cluster | replay-safety |
|
||||
| False claim of complete history | Wrong incident conclusions | Watermarks, gap markers, refusal on incomplete coverage | consistency |
|
||||
| API server load | Control plane harm | Explicit allowlist, watch from exact RV, backpressure, profiles, benchmarks | architecture |
|
||||
| SSA conflicts | Failed applies | Synthetic field manager, no force, dry run first | replay-safety |
|
||||
| Invalid owner references | Orphaned objects | Remove or remap UIDs, replay roots only | replay-safety |
|
||||
| Name collision | Community confusion | Rename or coordinate | none |
|
||||
|
||||
Each row of the risk table maps to at least one control above. The controls are structural. The journal format, the planner policy, and the RBAC manifests enforce them. They do not depend on operator discipline alone.
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Src["Source cluster API server"] -->|"get / list / watch, read-only"| KP["krply recorder and planner"]
|
||||
Audit["Audit logs"] -.->|optional| KP
|
||||
KP -->|journal write| J["Journal"]
|
||||
KP -->|"SSA create / patch, dry run first"| Tgt["Target sandbox cluster"]
|
||||
KP -->|"HTTP /v1 (read)"| UI["CLI and Web UI"]
|
||||
```
|
||||
|
||||
Key boundaries:
|
||||
|
||||
- **Source cluster**. The recorder identity is read-only. It uses get, list, and watch. It has no write verbs and no Secrets access. See recorder RBAC below.
|
||||
- **Journal**. The only write path is the journal writer. Any component that claims completeness must prove it from watermarks and gaps. It must never prove completeness from the absence of records.
|
||||
- **Target cluster**. The replay identity has create and patch only. It is bound only in the sandbox. It cannot delete. It cannot read Secrets. It never runs against the source cluster.
|
||||
- **Client boundary**. API reads go through the Query API. Historical queries always return coverage and gap information. A client cannot mistake a partial result for complete state.
|
||||
|
||||
## RBAC model (section 18)
|
||||
|
||||
The recorder identity is read-only with the minimum verbs:
|
||||
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
|
||||
Rules:
|
||||
|
||||
- Name specific API groups and resources. No wildcard groups, resources, or verbs.
|
||||
- No Secret access by default.
|
||||
- ConfigMaps and annotations can also contain sensitive material. Review them. This is why the ConfigMap comment in the RBAC manifests exists.
|
||||
- Replay uses a separate identity with narrow write permissions. It has create and patch only, plus get and list for the dry run.
|
||||
- No cluster-admin.
|
||||
- No write access to the source cluster.
|
||||
|
||||
Reference manifests: ../../deploy/rbac/. The Helm chart ships a recorder ClusterRole bound to its own ServiceAccount. See ../../deploy/helm/krply/.
|
||||
|
||||
## What the tool does NOT claim
|
||||
|
||||
- It does not claim that Kubernetes provides immutable history. It stores its own journal and shows gaps.
|
||||
- It does not guarantee a total order across resources. Ordering is per-stream.
|
||||
- It does not recreate external side effects, such as PV contents, databases, or webhook callbacks. It replays declarative state only.
|
||||
- It does not replace Kubernetes audit logging. Audit correlation is optional and works only when logs are available.
|
||||
- It does not hide uncertainty behind a green status. Partial results carry warnings.
|
||||
- It does not prove target convergence after apply. It observes without assuming.
|
||||
- It is not a backup tool. It does not restore secrets or stateful data.
|
||||
|
||||
## See also
|
||||
|
||||
- Event payload and provenance: ../event-schema/event-schema.md.
|
||||
- Consistency guarantees, meaning what a complete claim means: ../consistency/consistency.md.
|
||||
- Replay safety flow: ../replay-safety/replay-safety.md.
|
||||
- RBAC manifests: ../../deploy/rbac/.
|
||||
|
||||
[replay-safety]: ../replay-safety/replay-safety.md
|
||||
[consistency]: ../consistency/consistency.md
|
||||
Loading…
Add table
Add a link
Reference in a new issue