diff --git a/CLAUDE.md b/CLAUDE.md index ce260b93..8b71dba9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -381,6 +381,66 @@ Keep a small set of ANSI tests (~5-7) to smoke test visual output formatting. - Always strive to diagnose and address root causes, not symptoms - Empty strings, nil maps, missing fields must all be handled correctly +## Keeping Documentation in Sync + +The design doc at `design/design-doc-cli-diff.md` is hand-maintained and drifts quickly if it isn't updated alongside the +code. Before completing a change, check whether any of these triggers apply and update the corresponding section. + +**Triggers and where to update**: + +| Code change | Update | +|--------------------------------------------------------------------------------|----------------------------------------------------------------------------| +| Interface in `cmd/diff/diffprocessor/` (`DiffProcessor`, `CompDiffProcessor`, `DiffCalculator`, `ResourceManager`, `SchemaValidator`, `FunctionProvider`) — added/removed/renamed methods or changed signatures | `§6` of the design doc (the relevant interface block); `diff-processor-architecture.mermaid` and `package-overview-fixed.mermaid` | +| New or removed Kubernetes/Crossplane client in `cmd/diff/client/` | `§5.2.4`, `§6.9`; `client-architecture.mermaid`, `layered-architecture.mermaid`, `conceptual-layers.mermaid` | +| Renderer changes (`cmd/diff/renderer/`): new `OutputFormat`, new structured-output type, change to error-emission contract | `§6.8`; `diff-rendering-architecture.mermaid` | +| New CLI flag, renamed flag, removed flag, or new subcommand | `§8.1` examples; if it's a `ProcessorConfig` field too, `§6.1.2` | +| Project layout: new top-level package under `cmd/diff/`, removed file/dir | `§12.1` directory tree | +| Changes to the iterative render loop, eventual-state behaviour, or how `RequiredResources`/`RequiredSchemas` are consumed | `§9.5.6` (esp. `§9.5.6.2`) | +| Behavioural change to a domain component without a signature change (e.g. switching the schema-validation backend, moving defaulting in/out of validation, changing what errors propagate) | The relevant `§6` narrative paragraph; if it touches integration with upstream Crossplane libs, the matching `§9.5.x` block too | +| Workflow change: nested-XR handling, two-phase diff split, cleanup semantics, claim handling | `§7`; `diff-call-sequence.mermaid` | +| New integration-test category (e.g. a new structured-assertion pattern, a new claim edge case) | `§4` test-case list | +| A future-work item ships, or a new follow-up is identified | `§10` future enhancements | + +**Diagrams**: the `*.mermaid` sources under `design/design-doc-cli-diff/` are the source of truth. After editing a +mermaid file, regenerate its `.svg` so the rendered doc matches. Prefer the official mermaid-cli Docker image for +portability across machines and CI: + +```bash +cd design/design-doc-cli-diff +for f in *.mermaid; do + docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/data" minlag/mermaid-cli \ + -i "/data/$f" -o "/data/${f%.mermaid}.svg" +done +``` + +**README and E2E docs**: `README.md` documents user-facing CLI behaviour and output formats; update it for any change a +user would notice in their workflow — flag/subcommand changes, exit-code changes, structured-output schema changes +(new fields under `errors[]`, `validationFailures[]`, `impactAnalysis[]`, etc.), and human-readable rendering changes +that meaningfully alter what hits stdout/stderr. `test/e2e/README.md` covers E2E structure; update it when adding a new +test category, changing expectation-file conventions, or adding a new helper to the assertion harness. + +**When in doubt**, prefer updating the doc in the same PR as the code change. A single PR that ships drift is much +cheaper than retroactively auditing several PRs' worth of changes (as happened in the doc-refresh on 2026-06-15). + +## Git, Commits, Issues, and Pull Requests + +A few project-wide conventions apply to every change you submit to this repository: + +- **Sign every commit (DCO)**. This repo enforces the Developer Certificate of Origin. Always commit with `git commit -s` + so a `Signed-off-by:` trailer is added; PRs without DCO sign-off on every commit will be blocked from merging. If you + need to fix a missing sign-off after the fact, use `git commit --amend -s --no-edit` (or `git rebase --signoff` for a + range of commits). +- **Open PRs as drafts**. Always use `gh pr create --draft …` (or pass `draft: true` via the API). The maintainer can + mark the PR ready for review once they've assessed it; opening as a draft prevents premature reviewer notifications + and lets CI run before review starts. +- **Follow the repository's issue and PR templates**. The PR body must follow `.github/PULL_REQUEST_TEMPLATE.md` — + including the `### Description of your changes` heading, the `Fixes #` line (with a real issue number, or omit the + line entirely if there is none), and the "I have:" checklist. **Every checklist item must be either `[x]` checked or + struck through with `~~…~~`** — leaving items as `[ ]` is not acceptable. If a checklist item legitimately doesn't + apply (e.g. no e2e tests for a docs-only change), strike it through rather than skipping it. Issues filed via `gh + issue create` must use the matching template under `.github/ISSUE_TEMPLATE/` (`bug_report.md` or + `feature_request.md`); pass it via `--template bug_report.md` and fill in every section. + ## Related Documentation - [Design Document](design/design-doc-cli-diff.md): Comprehensive technical design and architecture diff --git a/README.md b/README.md index 14731286..f4159b92 100644 --- a/README.md +++ b/README.md @@ -513,7 +513,96 @@ The structured output includes: - **Full resource details**: apiVersion, kind, name, namespace - **Diff content**: old/new values for modifications, full spec for additions/removals - **Impact analysis** (comp only): which XRs are affected by composition changes and their status -- **Errors**: A top-level `errors` array with entries of the form `{ "resourceID": "...", "message": "..." }`, plus per-XR `error` fields in `impactAnalysis` for composition diffs +- **Errors**: A top-level `errors` array of `OutputError` objects (see [Validation Errors](#validation-errors) below for the schema and an example), plus per-XR `error` fields in `impactAnalysis` for composition diffs + +### Validation Errors + +When schema validation fails on the input XR or any rendered composed resource, `crossplane-diff` exits with code 2 and reports the failure in both human-readable and machine-readable form. + +**Human-readable output** (`crossplane-diff xr invalid-xr.yaml`): + +Per Unix convention, errors go to stderr and diff content goes to stdout. When validation is the only failure, stdout is empty and the structured failure detail appears on stderr, prefixed by an `ERROR: :` marker: + +``` +# stderr +ERROR: XNopResource/invalid-schema-xr: ns.diff.example.org/v1alpha1/XNopResource default/invalid-schema-xr: + spec.coolField: Invalid value: "number": ... [schema] +ns.nop.example.org/v1alpha1/XDownstreamResource default/invalid-schema-xr: + spec.forProvider.configData: Invalid value: "boolean": ... [schema] + +# stdout +(empty) +``` + +Each per-resource block has the shape `/ [/]:` followed by indented ` []` lines, where `` is one of `[schema]`, `[cel]`, `[unknownField]`, or `[defaulting]`. A bad value is appended as `(got )` when it isn't already substring-present in the message. When some inputs in a batched run succeed and others fail validation, the successful diffs appear on stdout and the failing inputs' `ERROR:` blocks appear on stderr. + +**Machine-readable output** (`crossplane-diff xr invalid-xr.yaml --output json`): + +```json +{ + "summary": { "added": 0, "modified": 0, "removed": 0 }, + "changes": [], + "errors": [ + { + "resourceID": "XNopResource/invalid-schema-xr", + "message": "ns.diff.example.org/v1alpha1/XNopResource default/invalid-schema-xr:\n spec.coolField: Invalid value: \"number\": ... [schema]\nns.nop.example.org/v1alpha1/XDownstreamResource default/invalid-schema-xr:\n spec.forProvider.configData: Invalid value: \"boolean\": ... [schema]", + "validationFailures": [ + { + "apiVersion": "ns.diff.example.org/v1alpha1", + "kind": "XNopResource", + "name": "invalid-schema-xr", + "namespace": "default", + "status": "invalid", + "errors": [ + { + "type": "schema", + "field": "spec.coolField", + "message": "spec.coolField: Invalid value: \"number\": ...", + "value": "number" + } + ] + }, + { + "apiVersion": "ns.nop.example.org/v1alpha1", + "kind": "XDownstreamResource", + "name": "invalid-schema-xr", + "namespace": "default", + "status": "invalid", + "errors": [ + { + "type": "schema", + "field": "spec.forProvider.configData", + "message": "spec.forProvider.configData: Invalid value: \"boolean\": ...", + "value": "boolean" + } + ] + } + ] + } + ] +} +``` + +The `OutputError` schema: + +| Field | Type | Description | +|-------|------|-------------| +| `resourceID` | string | Identifies which user-supplied input the diff was processing (one entry per batched run). Format: `/`. | +| `message` | string | Human-readable error string — the same text written to stderr. | +| `validationFailures` | `[]ResourceValidationFailure`, optional | Structured per-resource breakdown. Set only for schema-validation failures; `nil` for tool, IO, render, and scope-check errors. | + +`ResourceValidationFailure` carries `apiVersion`, `kind`, `name`, `namespace`, `status` (one of `"invalid"` or `"missingSchema"` — `"valid"` rows are filtered out), and `errors`, a list of `FieldValidationError` records: + +| Field | Type | Description | +|-------|------|-------------| +| `type` | string | `"schema"`, `"cel"`, `"unknownField"`, or `"defaulting"`. | +| `field` | string, optional | JSONPath of the offending field, when locatable. | +| `message` | string | Validator-emitted human-readable description; for k8s-derived schema errors this typically already embeds the field path and bad value. | +| `value` | any, optional | The offending value as the validator saw it. Type-preserved (string, number, bool, struct). | + +`resourceID` and `validationFailures` are intentionally complementary: `resourceID` anchors the failure to one user-supplied input, while `validationFailures` enumerates every resource (the input itself plus any composed resource) that failed validation under that input. They overlap on `kind`+`name` when the input itself is among the failing resources — that's deliberate, so consumers iterating `validationFailures` never miss an XR-level rejection. + +This same structure is used for `comp` output (under each composition's `errors` array) — see the composition diff JSON example above for placement. ## Exit Codes diff --git a/design/design-doc-cli-diff.md b/design/design-doc-cli-diff.md index 87c193b2..530ffd5d 100644 --- a/design/design-doc-cli-diff.md +++ b/design/design-doc-cli-diff.md @@ -231,36 +231,90 @@ test cases cover: - **Color Output**: Tests that color formatting is correctly applied when enabled. - **No-color Option**: Verifies that color codes are omitted when the `--no-color` flag is used. - **Summary Output**: Tests that the summary of changes (added, modified, removed) is correctly generated. - -These comprehensive test cases ensure that the Diff command functions correctly across the full range of Crossplane -resource types and composition patterns, providing confidence in its reliability for production use. The implementation -described in the following sections is designed to satisfy all these test cases. +- **Structured Output (JSON / YAML)**: Field-level assertions on `--output json` / `--output yaml` payloads using + `tu.AssertStructuredDiff` and `tu.AssertStructuredCompDiff` builders. These let tests assert on specific old/new + values and on summary counts without depending on ANSI escape sequences. Errors must appear in both stderr and the + structured output. +- **Typed Validation Failures**: Schema-validation tests use the fluent `WithError` / `WithValidationFailure` / + `WithFieldError` builder chain to pin specific GVKs, namespaces, statuses, error types (`schema`, `cel`, + `unknownField`, `defaulting`), and field paths. Message wording is intentionally not asserted so apimachinery + upgrades can shift phrasing without breaking tests. + +### 4.11 Composition Diff Scenarios + +The `comp` subcommand has its own set of integration tests: + +- **Affected XR Enumeration**: Verifies that all XRs using the proposed composition are correctly listed. +- **Composition-level Diff**: Tests that the top-level composition diff is rendered alongside per-XR impact. +- **Filtering**: Covers `--namespace`, `--resource [namespace/]name`, and `--include-manual`. The + `--namespace`/`--resource` mutual-exclusion error path is exercised. The `--resource` preflight (every named ref must + match at least one input composition) is exercised. +- **Update Policy Handling**: Verifies that `Manual` XRs are excluded by default and included with `--include-manual`, + and that both v1 (`spec.compositionUpdatePolicy`) and v2 (`spec.crossplane.compositionUpdatePolicy`) are honoured. +- **Downstream Field Changes**: Asserts field-level old/new values on composed resources of affected XRs. + +### 4.12 Nested XRs and Eventual State + +- **Nested XR Recursion**: Tests that composed XRs are themselves diffed, with identity preserved across renders by + fetching observed state. +- **`--max-nested-depth`**: Verifies the recursion limit short-circuits cleanly. +- **Two-phase Diff**: Verifies that resources rendered only by nested XRs are not falsely flagged as removals. +- **`--eventual-state`**: Tests multi-stage compositions (e.g., function-sequencer / `function-conditional`) whose + full effect requires multiple reconciliation cycles. + +### 4.13 Claim Edge Cases + +- **New Claim with `spec.claimRef`**: Verifies that compositions referencing + `.observed.composite.resource.spec.claimRef.*` render correctly for new claims (no backing XR yet exists), via the + synthesised dummy XR. +- **`crossplane.io/composite` Label**: Confirms that diffing a Claim does not show spurious changes to the composite + label, since Crossplane uses the XR name there even when rendering from a Claim. + +These comprehensive test cases ensure that both subcommands function correctly across the full range of Crossplane +resource types and composition patterns. The implementation described in the following sections is designed to satisfy +all these test cases. ## 5. Architecture Overview ### 5.1 High-Level Overview -The `crossplane-diff diff` command shows the changes that would result from applying Crossplane resources to a live -cluster. The command processes resources from files or stdin, compares them against the current state in the cluster, -and displays the differences in a familiar format. +The `crossplane-diff` binary exposes two diff subcommands and a `version` subcommand: + +- **`crossplane-diff xr [FILE]…`** (alias: `diff`) — given one or more XR or claim YAMLs, show the changes that would + result from applying them to the cluster. +- **`crossplane-diff comp [FILE]`** — given an updated Composition YAML, find every XR in the cluster that uses that + composition and show the impact of the composition change on each, including a top-level diff of the composition + itself. -The implementation leverages the existing Crossplane and/or Kubernetes machinery to: +Both subcommands process resources from files or stdin, compare them against the current state in the cluster, and +display differences in a familiar format. They share the same underlying per-XR rendering and diffing machinery — `comp` +delegates to the same `DiffProcessor` that `xr` uses, supplying the proposed composition through a `CompositionProvider` +callback. -1. Find the appropriate composition for a composite resource -2. Extract dependency information -3. Perform a simulated reconciliation -4. Use a dry-run approach to determine the changes without applying them +The implementation leverages existing Crossplane and Kubernetes machinery to: -The process flow is: +1. Find the appropriate composition for a composite resource (or use the user-supplied one for `comp`) +2. Extract dependency information from composition functions +3. Perform a simulated reconciliation through the render pipeline +4. Use server-side dry-run to determine resource-level changes without applying them +5. Walk the live resource tree to detect resources that would be removed + +The XR-diff flow is: 1. Load resources from files/stdin -2. For each resource: - 1. Find the matching composition - 2. Render what the resources would look like if applied (using `render`) - 3. While there are unresolved Requirements, fetch them and try to `render` again - 4. Validate the results against the schemas loaded into the cluster (using `beta validate`) - 5. Compare against current state in the cluster (using `beta trace` for child tracking) -3. Format and display differences +2. For each XR or claim: + 1. Resolve the matching composition + 2. Render the XR through the composition pipeline + 3. While the render reports new `RequiredResources` selectors, resolve them and re-render + 4. Recurse into any nested XRs (subject to `--max-nested-depth`), preserving their identity by fetching their + observed state from the cluster + 5. Validate the rendered tree against CRD/XRD schemas and enforce scope constraints + 6. Compare against current state in the cluster (server-side dry-run + tree walk) +3. Format and display differences in the configured output format + +The composition-diff flow layers on top of this: discover affected XRs in the cluster, optionally filter by namespace +or resource refs, drop XRs with `Manual` update policy unless `--include-manual` is set, run the per-XR flow above for +each, and aggregate into a `CompDiffOutput`. ### 5.2 Architectural Layers @@ -293,7 +347,8 @@ The orchestration layer that initializes the application context and coordinates **Key Components:** - `AppContext`: Holds application-wide dependencies and clients -- `DiffProcessor`: The main component responsible for executing the diff workflow +- `DiffProcessor`: Orchestrates per-XR diffing (used directly by `xr`, embedded by `CompDiffProcessor`) +- `CompDiffProcessor`: Orchestrates composition-impact diffing for `comp` - `Loader`: Handles loading resources from files or stdin **Responsibilities:** @@ -303,6 +358,7 @@ The orchestration layer that initializes the application context and coordinates - Process coordination - Resource loading - Result aggregation +- Cleanup of ephemeral state (Docker function containers, networks) #### 5.2.3 Domain Layer @@ -310,12 +366,14 @@ The business logic layer containing the core diff functionality, resource manage **Key Components:** -- `DiffCalculator`: Computes differences between resources -- `SchemaValidator`: Validates resources against their schemas -- `ResourceManager`: Manages resource-related operations -- `RequirementsProvider`: Resolves requirements for resource rendering -- `DiffRenderer`: Formats and displays diffs -- `Render Function`: Handles resource composition rendering; normally a reference to the `render` package +- `DiffCalculator`: Computes differences between resources (split into non-removal and removal phases for nested XRs) +- `SchemaValidator`: Validates resources against their schemas and enforces scope constraints +- `ResourceManager`: Fetches cluster state and manages owner references +- `RequirementsProvider`: Resolves environment-config and label-selector requirements between render iterations +- `FunctionProvider`: Resolves the function set for a composition, with strategies for caching and registry override +- `DiffRenderer` / `CompDiffRenderer`: Format and display XR and composition diffs (human-readable or structured + JSON/YAML) +- `Render Function`: Handles composition-pipeline rendering; normally a reference to the upstream `render` package **Responsibilities:** @@ -333,8 +391,8 @@ The infrastructure access layer that interfaces with Kubernetes and Crossplane. **Key Components:** - Kubernetes Clients: `ApplyClient`, `ResourceClient`, `SchemaClient`, `TypeConverter` -- Crossplane Clients: `CompositionClient`, `DefinitionClient`, `EnvironmentClient`, `FunctionClient`, - `ResourceTreeClient` +- Crossplane Clients: `CompositionClient`, `CompositionRevisionClient`, `DefinitionClient`, `EnvironmentClient`, + `FunctionClient`, `CredentialClient`, `ResourceTreeClient` **Responsibilities:** @@ -385,237 +443,491 @@ The data flow through the system follows these key steps: ### 6.1 DiffProcessor -The `DiffProcessor` is the central component that coordinates the diff workflow. It uses a dependency injection pattern -with factories for its subcomponents. +The `DiffProcessor` is the central component for XR diff. It uses a dependency injection pattern with factories for its +subcomponents and is also embedded inside the `CompDiffProcessor` (see §6.2), which delegates per-XR rendering to it. #### 6.1.1 Interfaces and Implementation ```go // DiffProcessor interface for processing resources. type DiffProcessor interface { - // PerformDiff processes all resources and produces a diff output - PerformDiff(ctx context.Context, stdout io.Writer, resources []*un.Unstructured) error - - // Initialize loads required resources like CRDs and environment configs - Initialize(ctx context.Context) error + // PerformDiff processes all resources and writes diff output to the configured writer. + // Returns (hasDiffs, error); a nil error with hasDiffs=false means everything matched the cluster. + PerformDiff(ctx context.Context, resources []*un.Unstructured, compositionProvider types.CompositionProvider) (bool, error) + + // DiffSingleResource processes one resource and returns its diffs without rendering them. + // Used by CompDiffProcessor to drive per-XR diffs. + DiffSingleResource(ctx context.Context, res *un.Unstructured, compositionProvider types.CompositionProvider) (map[string]*dt.ResourceDiff, error) + + // Initialize loads required resources like CRDs. + Initialize(ctx context.Context) error + + // Cleanup releases resources held by the processor (in particular, Docker function containers + // started during rendering — without this they leak for the lifetime of the process). + Cleanup(ctx context.Context) error } ``` -The `DefaultDiffProcessor` implements this interface and uses several subcomponents: +A `CompositionProvider` is `func(ctx, *Unstructured) (*apiextensionsv1.Composition, error)`. The `xr` subcommand passes a +provider that looks up matching compositions in the cluster; the `comp` subcommand passes one backed by the updated +composition file under test, so the same per-XR diff machinery serves both flows. -- `fnClient`: Handles function-related operations -- `compClient`: Handles composition-related operations -- `schemaValidator`: Validates resources against schemas +The `DefaultDiffProcessor` uses several subcomponents: + +- `fnProvider`: Resolves the function set for a given composition (see §6.6) +- `compClient`, `defClient`, `schemaClient`, `treeClient`, `applyClient`: Cluster I/O (see §6.8) +- `schemaValidator`: Validates resources against schemas and enforces scope constraints - `diffCalculator`: Calculates differences between resources -- `diffRenderer`: Formats and displays diffs -- `requirementsProvider`: Handles requirements for resource rendering +- `diffRenderer`: Formats and displays XR diffs +- `requirementsProvider`: Handles requirements (env-configs, label selectors) for rendering #### 6.1.2 Configuration The `ProcessorConfig` structure provides configuration options: -- `Namespace`: The namespace for resources -- `Colorize`: Whether to colorize the output -- `Compact`: Whether to show compact diffs -- `Logger`: The logger to use -- `RenderFunc`: The function to use for rendering resources -- `Factories`: Factory functions for creating components +- `Namespace`: Default namespace, used by `comp` flow when listing XRs (`xr` ignores it; XRs carry their own namespace). +- `Colorize`, `Compact`: Visual formatting toggles for the human-readable renderer. +- `OutputFormat`: One of `diff`, `json`, `yaml`. Selects between the human-readable and structured renderers. +- `MaxNestedDepth`: Recursion limit for nested-XR diff (`--max-nested-depth`). +- `MaxRenderIterations`: Cap on the requirements-discovery loop (`--max-iterations`). +- `IncludeManual`: For `comp`, also consider XRs whose composition update policy is `Manual`. +- `EventualState`: Synthesize composed-resource readiness between render iterations to model the steady state of + multi-stage compositions (`--eventual-state`). +- `IgnorePaths`: Field paths to suppress from diffs (e.g., status fields known to be reconciler-set). +- `FunctionCredentials`: Image-pull credentials for private function registries. +- `FunctionRegistryOverride`: Rewrites function image references to a mirror. +- `CrossplaneRenderBinary`: Optional path to an external `crossplane render` binary (otherwise the in-process render + package is used). +- `Stdout`, `Stderr`: Output sinks (writers are no longer threaded through method calls). +- `Logger`: Structured logger, propagated to all subcomponents. +- `RenderFunc`: Renders a composition pipeline; defaults to the in-process engine. +- `Factories`: Factory functions for creating subcomponents (used for testing and to swap caching strategies). + +### 6.2 CompDiffProcessor + +The `CompDiffProcessor` powers the `comp` subcommand. Where `DiffProcessor` answers "what changes if I apply this XR?", +`CompDiffProcessor` answers "what changes for every XR currently using this composition if I update it?" + +#### 6.2.1 Interface and Implementation -### 6.2 DiffCalculator +```go +// CompDiffProcessor defines the interface for composition diffing. +type CompDiffProcessor interface { + // DiffComposition processes composition changes and shows impact on existing XRs. + // When `resources` is non-empty, impact analysis is restricted to the named composites. + DiffComposition(ctx context.Context, compositions []*un.Unstructured, namespace string, resources []k8stypes.NamespacedName) (bool, error) + + Initialize(ctx context.Context) error + Cleanup(ctx context.Context) error +} +``` + +`DefaultCompDiffProcessor` embeds a `DiffProcessor` and a `CompositionClient` and orchestrates: + +1. **Discover affected XRs.** For each input composition: list cluster XRs whose `compositionRef`/`compositionSelector` + resolves to it. Optional filters: `--namespace` (scope to one namespace), `--resource` (limit to specific composite + names, mutually exclusive with `--namespace`). +2. **Partition by update policy.** XRs with `compositionUpdatePolicy: Manual` are dropped unless `--include-manual` is + set, since they would not be affected by a composition change anyway. +3. **Diff the composition itself.** Compute a top-level diff between the proposed composition and the cluster's current + version, surfaced as `CompositionDiff`. +4. **Diff each XR.** Delegate to the embedded `DiffProcessor` via `DiffSingleResource`, supplying a + `CompositionProvider` that returns the proposed composition for the affected XR's GVK and the cluster's composition + otherwise (so nested XRs that use a different composition are diffed against their unchanged composition). +5. **Aggregate.** Produce a `CompDiffOutput` with composition-level changes, an `XRImpact` entry per XR, and an + `AffectedResourcesSummary` (changed / unchanged / errored counts). + +The processor deliberately does not default its own `RenderFunc` (it routes rendering through the embedded `xrProc`), +because `NewEngineRenderFn` allocates a Docker bridge network whose teardown lives on the XR processor's `Cleanup`. + +### 6.3 DiffCalculator The `DiffCalculator` is responsible for calculating differences between resources. -#### 6.2.1 Interfaces and Implementation +#### 6.3.1 Interface and Implementation ```go // DiffCalculator calculates differences between resources. type DiffCalculator interface { - // CalculateDiff computes the diff for a single resource - CalculateDiff(ctx context.Context, composite *un.Unstructured, desired *un.Unstructured) (*dt.ResourceDiff, error) - - // CalculateDiffs computes all diffs for the rendered resources and identifies resources to be removed - CalculateDiffs(ctx context.Context, xr *cmp.Unstructured, desired render.Outputs) (map[string]*dt.ResourceDiff, error) - - // CalculateRemovedResourceDiffs identifies resources that would be removed and calculates their diffs - CalculateRemovedResourceDiffs(ctx context.Context, xr *un.Unstructured, renderedResources map[string]bool) (map[string]*dt.ResourceDiff, error) + // CalculateDiff computes the diff for a single resource (used for the XR itself and for the composition diff). + CalculateDiff(ctx context.Context, composite *un.Unstructured, desired *un.Unstructured) (*dt.ResourceDiff, error) + + // CalculateDiffs is the convenience entry point: computes non-removal diffs and removal diffs in sequence + // for a single (non-nested) render. + CalculateDiffs(ctx context.Context, xr *cmp.Unstructured, desired render.CompositionOutputs) (map[string]*dt.ResourceDiff, error) + + // CalculateNonRemovalDiffs computes diffs for everything in the rendered set, returning the diffs and + // a set of rendered resource keys. Splitting this from removal detection lets nested XRs be processed + // before any "missing from render" decisions are made (a nested XR may render additional resources + // that the parent's render does not see). + CalculateNonRemovalDiffs(ctx context.Context, xr *cmp.Unstructured, parentComposite *un.Unstructured, desired render.CompositionOutputs) (map[string]*dt.ResourceDiff, map[string]bool, error) + + // CalculateRemovedResourceDiffs identifies resources that exist in the cluster under this XR but are + // absent from the merged set of rendered resource keys, and produces removal diffs for them. + CalculateRemovedResourceDiffs(ctx context.Context, xr *un.Unstructured, renderedResources map[string]bool) (map[string]*dt.ResourceDiff, error) } ``` +The two-phase split (`CalculateNonRemovalDiffs` + `CalculateRemovedResourceDiffs`) is load-bearing: when an XR contains +nested XRs, the parent must finish processing its children before the framework can decide what counts as "removed". +Otherwise nested-only resources would be falsely flagged as removals because they don't appear in the parent's render +output. + The `DefaultDiffCalculator` handles: -- Retrieving current resources from the cluster -- Performing dry-run applies to see what would happen -- Generating text-based diffs between resources +- Retrieving current resources from the cluster (via `ResourceManager`) +- Performing dry-run applies to determine the would-be state +- Generating text-based diffs between current and desired - Identifying resources that would be removed -### 6.3 ResourceManager +### 6.4 ResourceManager -The `ResourceManager` handles resource-related operations such as fetching current resources and managing ownership -references. +The `ResourceManager` handles resource-related operations such as fetching current resources, walking the live resource +tree, and managing ownership references. ![Loader and Validator Architecture](./assets/design-doc-cli-diff/loader-validator-architecture.svg) *Figure 8: Resource loading and validation architecture showing how resources are loaded and validated* -#### 6.3.1 Interfaces and Implementation +#### 6.4.1 Interface and Implementation ```go // ResourceManager handles resource-related operations like fetching, updating owner refs, // and identifying resources to be removed. type ResourceManager interface { - // FetchCurrentObject retrieves the current state of an object from the cluster - FetchCurrentObject(ctx context.Context, composite *un.Unstructured, desired *un.Unstructured) (*un.Unstructured, bool, error) - - // UpdateOwnerRefs ensures all OwnerReferences have valid UIDs - UpdateOwnerRefs(parent *un.Unstructured, child *un.Unstructured) + // FetchCurrentObject retrieves the current state of an object from the cluster. + // Used for both name-based and generateName/label-based lookup. + FetchCurrentObject(ctx context.Context, composite *un.Unstructured, desired *un.Unstructured) (*un.Unstructured, bool, error) + + // UpdateOwnerRefs ensures all OwnerReferences have valid UIDs (synthesizing dry-run UIDs + // for resources that don't yet exist). + UpdateOwnerRefs(ctx context.Context, parent *un.Unstructured, child *un.Unstructured) + + // FetchObservedResources walks the live resource tree under an XR and returns the composed + // resources observed in the cluster. Used to preserve the identity of nested XRs across + // re-renders (so re-rendering doesn't appear to "create" a child XR that already exists). + FetchObservedResources(ctx context.Context, xr *cmp.Unstructured) ([]cpd.Unstructured, error) } ``` The `DefaultResourceManager` handles: - Looking up resources by name -- Looking up resources by labels and annotations -- Handling resources with generateName -- Managing owner references +- Looking up resources by labels and annotations (`crossplane.io/composition-resource-name`, claim-name labels) for + resources rendered with `generateName` +- Walking the resource tree (via `ResourceTreeClient`) to enumerate observed children of an XR +- Managing owner references and synthesizing UIDs for dry-run -### 6.4 SchemaValidator +### 6.5 SchemaValidator -The `SchemaValidator` validates resources against their schemas, ensuring they are valid before calculating diffs. +The `SchemaValidator` validates resources against their schemas and enforces Crossplane v2 scope rules before diffs are +calculated. -#### 6.4.1 Interfaces and Implementation +#### 6.5.1 Interface and Implementation ```go // SchemaValidator handles validation of resources against CRD schemas. type SchemaValidator interface { - // ValidateResources validates resources using schema validation - ValidateResources(ctx context.Context, xr *un.Unstructured, composed []cpd.Unstructured) error - - // EnsureComposedResourceCRDs ensures we have all required CRDs for validation - EnsureComposedResourceCRDs(ctx context.Context, resources []*un.Unstructured) error + // ValidateResources validates resources using schema validation. + ValidateResources(ctx context.Context, xr *un.Unstructured, composed []cpd.Unstructured) error + + // EnsureComposedResourceCRDs ensures we have all required CRDs for validation, fetching them + // lazily from the cluster the first time a GVK is encountered. + EnsureComposedResourceCRDs(ctx context.Context, resources []*un.Unstructured) error + + // ValidateScopeConstraints enforces Crossplane v2 scope rules: namespaced XRs cannot own + // cluster-scoped composed resources (claims are the only allowed cluster→namespace exception), + // and namespaced composed resources must live in the XR's namespace. + ValidateScopeConstraints(ctx context.Context, resource *un.Unstructured, expectedNamespace string, isClaimRoot bool) error } ``` The `DefaultSchemaValidator` handles: -- Loading CRDs from the cluster -- Converting XRDs to CRDs -- Validating resources against schemas +- Loading CRDs from the cluster on demand +- Converting XRDs to CRDs (so XRs can be schema-validated) +- Validating resources against those schemas via the upstream `pkg/validate.SchemaValidate` structured API +- Enforcing scope constraints across the rendered tree + +`SchemaValidate` returns a `*pkgvalidate.ValidationResult` with typed per-resource records — `ResourceValidationResult` +entries containing `FieldValidationError` records with type tags (`schema`, `cel`, `unknownField`, `defaulting`), field +paths, messages, and offending values. The validator surfaces this structure to callers in two ways: + +- A `SchemaValidationError` carries the `*ValidationResult` so error-handling code can inspect typed failures + programmatically. +- The structured-output renderers expose it on the wire as `OutputError.ValidationFailures` (see §6.8.3), so JSON/YAML + consumers don't need to parse the human-readable `Message` string. -### 6.5 RequirementsProvider +Note that `SchemaValidate` deep-copies its inputs and does not mutate them. The previous, line-parsing API +(`validate.SchemaValidation`) fused defaulting with validation as a side-effect; with the new API, defaulting is +explicit. The processor calls `clixr.ApplyCRDDefaults` (renamed from the old `render.DefaultValues`) on the rendered +tree before invoking `ValidateResources`, preserving the invariant that the diff calculator sees fully-defaulted +resources. -The `RequirementsProvider` handles requirements for resource rendering, providing additional resources needed by -Crossplane functions. +### 6.6 RequirementsProvider -#### 6.5.1 Implementation +The `RequirementsProvider` provides extra resources that composition functions ask for via `RequiredResources`. It is a +concrete struct (not an interface) and is owned by the XR processor. + +#### 6.6.1 Implementation The `RequirementsProvider` handles: -- Caching frequently used resources -- Fetching resources by name or label selectors -- Converting between resource formats +- Caching frequently used resources to avoid re-fetching across the iterative render loop +- Fetching resources by name or label selector, scoped to the XR's namespace where appropriate +- Loading EnvironmentConfigs as a baseline available to every render +- Synthesizing a dummy backing XR (with `spec.claimRef`) when diffing a brand-new claim, so that compositions + referencing `.observed.composite.resource.spec.claimRef.*` render correctly even though Crossplane has not yet + populated `claimRef` on a real backing XR + +### 6.7 FunctionProvider + +The `FunctionProvider` is the seam between the diff tool and Crossplane composition functions. Function containers can +be expensive to spin up, so this layer governs how — and how aggressively — they are reused across renders. + +#### 6.7.1 Interface and Implementations + +```go +// FunctionProvider resolves the function set used by a given composition. +type FunctionProvider interface { + GetFunctionsForComposition(comp *apiextensionsv1.Composition) ([]pkgv1.Function, error) + Cleanup(ctx context.Context) error +} +``` + +Three implementations are wired up at the CLI layer based on configuration: + +- **`DefaultFunctionProvider`**: Fetches the function definitions from the cluster on demand. Used when the same + composition is unlikely to be rendered repeatedly. +- **`CachedFunctionProvider`**: Lazy-loads functions per composition and caches them by composition name. Composition + diff renders the same composition many times (once per affected XR), so cached function containers are reused across + XRs in the same run. The factory selection happens at the CLI layer; the processors are oblivious to which strategy + is in use. +- **`RegistryOverrideFunctionProvider`**: Wraps another provider and rewrites function image references to a mirror + (used with `--function-registry-override`). + +`Cleanup` is responsible for terminating any function containers the provider has started; it is invoked by the +processor's `Cleanup`. -### 6.6 DiffRenderer +### 6.8 DiffRenderer and CompDiffRenderer -The `DiffRenderer` formats and displays diffs in a human-readable format. +The renderer layer formats diff results. It is split into two interfaces — one per subcommand — and each has a +human-readable and a structured (JSON/YAML) implementation. ![Diff Rendering Architecture](./assets/design-doc-cli-diff/diff-rendering-architecture.svg) *Figure 6: Diff rendering architecture showing how diffs are formatted and displayed* -#### 6.6.1 Interfaces and Implementation +#### 6.8.1 Interfaces ```go -// DiffRenderer handles rendering diffs to output. +// DiffRenderer handles rendering XR diffs. type DiffRenderer interface { - // RenderDiffs formats and outputs diffs to the provided writer - RenderDiffs(stdout io.Writer, diffs map[string]*dt.ResourceDiff) error + // RenderDiffs writes per-resource diffs and any per-resource errors. The output writer is held + // by the renderer (configured at construction time), not passed in per call, so the same + // interface can serve human-readable and structured renderers without leaking io.Writer. + RenderDiffs(diffs map[string]*dt.ResourceDiff, errs []dt.OutputError) error +} + +// CompDiffRenderer handles rendering composition diffs. +type CompDiffRenderer interface { + RenderCompDiff(output *CompDiffOutput) error } ``` -The `DefaultDiffRenderer` handles: +Implementations: + +- `DefaultDiffRenderer` / `DefaultCompDiffRenderer`: Human-readable colored output (or plain text under `--no-color`), + with optional `--compact` mode for large diffs. +- `StructuredDiffRenderer` / `StructuredCompDiffRenderer`: Emit JSON or YAML controlled by `--output {json,yaml}`. The + YAML encoder uses `sigs.k8s.io/yaml`, so JSON struct tags are reused for YAML field names. + +#### 6.8.2 Output format selection and error contract -- Formatting diffs with colors and context -- Supporting compact mode for large diffs -- Summarizing changes +The selection is via `OutputFormat` on `ProcessorConfig`: -### 6.7 Kubernetes and Crossplane Clients +```go +type OutputFormat string +const ( + OutputFormatDiff OutputFormat = "diff" // default; human-readable + OutputFormatJSON OutputFormat = "json" + OutputFormatYAML OutputFormat = "yaml" +) +``` + +Under `json`/`yaml`, errors are written to **both** stderr (for human visibility) and the structured output (so CI/CD +pipelines can parse them programmatically). The structured renderers always emit a valid document — even when every XR +fails — by attaching errors to an `OutputError` field. `OutputError` uses JSON struct tags only (the YAML library reads +JSON tags), so the field naming is consistent across formats. + +#### 6.8.3 Structured output types + +The structured types (in `cmd/diff/renderer/types/`) are part of the public output contract: + +- `StructuredDiffOutput` — XR diff: per-resource diffs with field-level old/new, summary counts, errors. +- `CompDiffOutput` — composition diff: top-level `CompositionDiff`, list of `XRImpact`, `AffectedResourcesSummary`, + `DownstreamChanges`. +- `XRImpact`, `XRStatus`, `DownstreamChanges`, `AffectedResourcesSummary` — finer-grained components used by the test + harness for field-level assertions. +- `OutputError` — error envelope used by both XR and comp diff outputs. Carries: + - `ResourceID`: which user-supplied input the diff was processing (one entry per batched run) + - `Message`: human-readable error string + - `ValidationFailures`: optional `[]ResourceValidationFailure`, populated when the error originated from schema + validation. Lets machine consumers inspect typed failures without parsing `Message`. +- `ResourceValidationFailure` — per-resource view inside `ValidationFailures`. Mirrors upstream + `pkg/validate.ResourceValidationResult` (apiVersion / kind / name / namespace / status), but is owned by + `crossplane-diff` so the public JSON schema can evolve independently of upstream's. `Status` surfaces `"invalid"` and + `"missingSchema"`; valid entries are filtered out so consumers iterating `ValidationFailures` see only failure rows. +- `FieldValidationError` — single field-level error inside `ResourceValidationFailure.Errors`. Carries `Type` + (`"schema"` / `"cel"` / `"unknownField"` / `"defaulting"`), `Field` (JSONPath, when locatable), `Message`, and + `Value` (typed: string, number, bool, or struct). + +`ResourceID` and `ValidationFailures` are intentionally complementary: `ResourceID` anchors the failure to a specific +batched input, while `ValidationFailures` enumerates every resource (the input itself plus any composed resources) +that failed validation under that input. They overlap on Kind+Name when the input itself is among the failing +resources — that's deliberate, so consumers iterating `ValidationFailures` never miss an XR-level rejection. +`ValidationFailures` is `nil` for non-validation paths (scope check, tool errors, IO errors). + +### 6.9 Kubernetes and Crossplane Clients The client layer provides interfaces to interact with Kubernetes and Crossplane resources. ![Client Architecture](./assets/design-doc-cli-diff/client-architecture.svg) *Figure 5: Kubernetes and Crossplane client architecture showing the interfaces and implementations* -#### 6.7.1 Kubernetes Clients +#### 6.9.1 Kubernetes Clients -- `ApplyClient`: Handles server-side apply operations -- `ResourceClient`: Handles basic CRUD operations -- `SchemaClient`: Handles schema-related operations -- `TypeConverter`: Handles conversion between Kubernetes types +- `ApplyClient`: Handles server-side dry-run apply +- `ResourceClient`: Handles basic CRUD operations against the dynamic client +- `SchemaClient`: Handles schema-related operations (fetching CRDs, scope detection) +- `TypeConverter`: Handles GVK ↔ GVR resolution and resource-name lookup -#### 6.7.2 Crossplane Clients +#### 6.9.2 Crossplane Clients -- `CompositionClient`: Handles composition-related operations -- `DefinitionClient`: Handles definition-related operations -- `EnvironmentClient`: Handles environment-related operations -- `FunctionClient`: Handles function-related operations -- `ResourceTreeClient`: Handles resource tree operations +- `CompositionClient`: Finds and fetches Compositions +- `CompositionRevisionClient`: Fetches Composition Revisions (groundwork for revision-aware diffing) +- `DefinitionClient`: Fetches XRDs and resolves XR/claim relationships +- `EnvironmentClient`: Fetches EnvironmentConfigs +- `FunctionClient`: Fetches Function package definitions and per-composition pipelines +- `CredentialClient`: Resolves function image-pull credentials referenced by `--function-credentials` +- `ResourceTreeClient`: Walks parent/child resource relationships in the cluster ## 7. Key Workflows ![Call Sequence](./assets/design-doc-cli-diff/diff-call-sequence.svg) *Figure 7: Call sequence diagram showing the interaction between components during a diff operation* -### 7.1 Diff Workflow - -1. The `Cmd` parses arguments and initializes the application context -2. The `Loader` loads resources from files or stdin -3. The `DiffProcessor` initializes and loads required schemas -4. For each resource: - - The `DiffProcessor` finds the matching composition - - The `DiffProcessor` renders the resource using the render function - - The `RequirementsProvider` resolves any requirements - - The `DiffProcessor` propagates namespaces from XR to managed resources (`propagateNamespacesToManagedResources`) - - The `SchemaValidator` validates the rendered resources and enforces scope constraints (`ValidateScopeConstraints`) - - The `DiffCalculator` calculates diffs between current and desired states - - The `DiffRenderer` formats and displays the diffs - -### 7.2 Resource Rendering Workflow - -1. The `DiffProcessor` calls the render function with the XR and composition +### 7.1 XR Diff Workflow + +1. `Cmd` parses arguments and initializes the application context. +2. The `Loader` loads resources from files or stdin. +3. `DiffProcessor.Initialize` loads required schemas. +4. For each input XR or claim: + - The `DiffProcessor` resolves the matching composition (or, for `comp`, the proposed one supplied via the + `CompositionProvider`). + - It calls `RenderToStableState` (see §9.5.6.2), which iteratively renders the composition pipeline, resolves any + `RequiredResources` selectors via the `RequirementsProvider`, and re-renders until the requirement set stabilises + (or the eventual-state criterion is met under `--eventual-state`). + - For any nested XRs in the rendered output, the `ResourceManager` fetches their observed state from the cluster to + preserve identity, then the processor recurses (subject to `--max-nested-depth`). + - The processor strips namespaces from cluster-scoped composed resources (workaround for upstream + `SetComposedResourceMetadata` blindly setting namespaces; see §9.5.6.3). + - The `SchemaValidator` validates the rendered resources and enforces scope constraints + (`ValidateScopeConstraints`). + - `DiffCalculator.CalculateNonRemovalDiffs` computes per-resource diffs for the entire (possibly nested) tree. + - Once the whole tree has been processed, `DiffCalculator.CalculateRemovedResourceDiffs` identifies resources that + exist in the cluster under this XR but no longer appear in the rendered set. + - The `DiffRenderer` (human-readable or structured) formats and displays the result. +5. `Cleanup` tears down any function containers / networks created during rendering. This is essential — without it, + Docker resources leak for the lifetime of the process. + +### 7.2 Composition Diff Workflow + +1. The `Loader` loads the proposed composition(s). +2. `CompDiffProcessor.Initialize` loads schemas and prepares the embedded XR processor. +3. For each input composition: + - List XRs in the cluster that resolve to this composition (`compositionRef` or `compositionSelector`). Optional + filters: `--namespace`, `--resource [namespace/]name` (mutually exclusive). When `--resource` is supplied, a + preflight pass ensures every named ref is relevant to at least one input composition; otherwise the call fails + before any rendering happens. + - Drop XRs with `compositionUpdatePolicy: Manual` unless `--include-manual` is set. + - Calculate the composition's own diff against the cluster's current version. + - For each remaining XR, run the XR diff workflow above, using a `CompositionProvider` that returns the proposed + composition for the affected XR's GVK and the cluster's composition for any nested XRs of a different kind. +4. Aggregate per-XR results into a `CompDiffOutput` (composition diff + `XRImpact` list + + `AffectedResourcesSummary`) and render via the `CompDiffRenderer`. + +### 7.3 Resource Rendering Workflow + +1. The `DiffProcessor` calls the render function with the XR, composition, function set, and any requirements gathered + so far. 2. The render function executes the composition pipeline: - It sets up the initial state with the XR - It executes each function in the pipeline - - It returns the desired state with the XR and composed resources -3. The `DiffProcessor` resolves any requirements and reruns the render function if needed -4. The rendered resources are validated and used for diff calculation + - It returns the desired state with the XR, composed resources, and `RequiredResources` / `RequiredSchemas` + selectors describing what additional inputs the functions need on the next pass +3. The `DiffProcessor` resolves any new requirements via the `RequirementsProvider` and reruns the render function if + the requirement set grew. Under `--eventual-state` it additionally synthesizes Ready conditions on rendered composed + resources between iterations to model multi-stage compositions. +4. The rendered resources are validated and used for diff calculation. ## 8. Implementation Details ### 8.1 Basic Usage -The command's basic usage would be: +The binary exposes two diff subcommands: ``` -crossplane-diff diff [FILE]... +crossplane-diff xr [FILE]... # alias: crossplane-diff diff [FILE]... +crossplane-diff comp [FILE] ``` -Similar to `kubectl diff`, the command will: +Similar to `kubectl diff`, both: 1. Accept input from files or stdin (when `-` is specified) -2. Process multiple files when provided +2. Process multiple files when provided (`xr` only; `comp` takes a single composition file) 3. Display a diff of the changes that would be made if the resources were applied -Examples: +`xr` examples: ``` # Show changes that would result from applying an XR from a file -crossplane-diff diff xr.yaml +crossplane-diff xr xr.yaml # Show changes from stdin -cat xr.yaml | crossplane-diff diff - +cat xr.yaml | crossplane-diff xr - # Process multiple files -crossplane-diff diff xr1.yaml xr2.yaml xr3.yaml +crossplane-diff xr xr1.yaml xr2.yaml xr3.yaml # Show changes in a compact format with minimal context -crossplane-diff diff --compact xr.yaml +crossplane-diff xr --compact xr.yaml + +# Emit machine-readable output for CI/CD +crossplane-diff xr --output json xr.yaml +crossplane-diff xr --output yaml xr.yaml + +# Limit nested-XR recursion +crossplane-diff xr --max-nested-depth 3 xr.yaml + +# Show steady-state diff for compositions that need multiple reconciliation cycles +crossplane-diff xr --eventual-state xr.yaml +``` + +`comp` examples: ``` +# Show impact of a composition change on every XR using it +crossplane-diff comp updated-composition.yaml + +# Restrict impact analysis to one namespace +crossplane-diff comp updated-composition.yaml -n production + +# Restrict to specific composites by [namespace/]name (mutually exclusive with -n) +crossplane-diff comp updated-composition.yaml --resource production/my-xr --resource other-xr + +# Also include XRs whose update policy is Manual +crossplane-diff comp updated-composition.yaml --include-manual +``` + +Note that the `xr` subcommand has no `--namespace` flag: namespaced XRs carry their own namespace in YAML, and that +namespace flows through render, validation, dry-run apply, and requirement resolution. The `comp` subcommand's +`--namespace` flag scopes which existing XRs are considered, not which namespace XRs render in. ### 8.2 Output Format @@ -770,21 +1082,40 @@ process YAML resources. #### 9.5.2 Schema Validation -The command integrates with the schema validation code from the `validate` command: +The command integrates with Crossplane CLI's structured validation API at `pkg/validate.SchemaValidate`: ```go -// Use the validation logic from the validate command -if err := validate.SchemaValidation(ctx, resources, v.crds, true, true, loggerWriter); err != nil { - return errors.Wrap(err, "schema validation failed") +// Apply CRD defaults explicitly (the structured API doesn't mutate inputs). +for _, r := range resources { + if err := clixr.ApplyCRDDefaults(r.Object, r.GetAPIVersion(), *crd); err != nil { + return errors.Wrap(err, "apply CRD defaults") + } +} + +// SchemaValidate is the structured-result API: it returns a +// *ValidationResult that callers inspect directly. +result, err := pkgvalidate.SchemaValidate(ctx, resources, v.schemaClient.GetAllCRDs()) +if err != nil { + return NewSchemaValidationError(resourceID, "schema validation setup failed", err) +} +if rerr := pkgvalidate.ResultError(result, true); rerr != nil { + return NewSchemaValidationError(resourceID, formatValidationErrors(result), rerr).WithResult(result) } ``` This validation: -- Ensures resources conform to their CRD schemas -- Provides consistent validation messages across commands -- Shares the same validation rules as other Crossplane tools +- Ensures resources conform to their CRD schemas (and CEL rules) via `pkg/validate.SchemaValidate` +- Returns typed `FieldValidationError` records (no stdout-parsing) that flow into `SchemaValidationError.Result` and, + for structured output, into `OutputError.ValidationFailures` +- Treats missing schemas as a failure via `pkgvalidate.ResultError(result, true)`, preserving the historical + error-on-missing-schemas semantic +- Shares validation rules with other Crossplane tools that consume the same `pkg/validate` package - Reduces code duplication and maintenance burden +Defaulting is explicit (via `clixr.ApplyCRDDefaults`) rather than fused with validation as it was under the old +`validate.SchemaValidation` API. This decouples the two concerns: defaulting can fail independently and is reported as +a `FieldErrorTypeDefaulting` entry in the structured result. + #### 9.5.3 Resource Rendering The Diff command calls the same `render.Render` function used by other components: @@ -836,85 +1167,83 @@ modular design, while ensuring the Diff command integrates seamlessly into the b #### 9.5.6 Modifications to Existing Components and Integration Challenges While leveraging existing components provides numerous benefits, it also required some modifications to ensure they meet -the needs of the Diff command: +the needs of the Diff command. Some of these have since been upstreamed. -##### Modified Render Output Contract +##### 9.5.6.1 Render Output Contract -The most significant change was the alteration of the `render.Outputs` structure to add `Requirements` as a member: +Earlier versions of the diff tool carried a custom `render.Outputs` struct with an added +`Requirements map[string]fnv1.Requirements` field. Since then, upstream Crossplane CLI has evolved its render output to +support requirements discovery natively. The diff tool now consumes the upstream +`github.com/crossplane/cli/v2/cmd/crank/render` package directly: ```go -// Outputs contains all outputs from the render process. -type Outputs struct { - // the rendered xr +// CompositionOutputs contains all outputs from the render process. +type CompositionOutputs struct { + // The rendered XR CompositeResource *ucomposite.Unstructured - // the rendered mrs derived from the xr + // The rendered MRs derived from the XR ComposedResources []composed.Unstructured - // the Function results (not render results) - Results []unstructured.Unstructured - // the Crossplane context object - Context *unstructured.Unstructured - // the Function requirements - Added for Diff command - Requirements map[string]fnv1.Requirements + // Function results (not render results) + Results []kunstructured.Unstructured + // The Crossplane context object + Context *kunstructured.Unstructured + // Resource selectors that functions asked the orchestrator to resolve before the next render iteration + RequiredResources []*fnv1.ResourceSelector + // Schema selectors that functions asked the orchestrator to resolve before the next render iteration + RequiredSchemas []*fnv1.SchemaSelector } ``` -This change was necessary to support the iterative requirements discovery process in the Diff command, which needs to: -1. Capture requirements from the render process -2. Resolve those requirements -3. Re-render with the resolved requirements -4. Detect when no new requirements are needed +Note that the field is now `RequiredResources` (a slice of selectors), not the per-function map of `Requirements` the +diff tool previously carried. `RequiredSchemas` is also a new addition the diff tool consumes when functions need CRD +schemas at render time. -This modification required coordination with the maintainers of the `render` package to ensure backward compatibility -while adding this functionality. +##### 9.5.6.2 Iterative Render to Stable State -##### Render Iterative Requirements Loop - -The Diff command needed to implement an iterative requirements discovery process that wasn't previously needed in other -commands. This required developing a new pattern to handle the discovery and resolution of requirements: +The diff tool drives an iterative render loop to discover and fulfil function requirements (and, optionally, to model +multi-stage compositions reaching steady state). The current implementation is a method on `DefaultDiffProcessor`: ```go -// RenderWithRequirements performs an iterative rendering process that discovers and fulfills requirements. -func (p *DefaultDiffProcessor) RenderWithRequirements( +// RenderToStableState iteratively renders the composition pipeline until either: +// - the requirements set stabilises (synthesizeReady=false, the default), or +// - no new composed resources appear and Ready conditions stabilise (synthesizeReady=true, +// used by --eventual-state). +// +// Between iterations it resolves any new RequiredResources via the RequirementsProvider and, +// when synthesizeReady is true, synthesises Ready=True conditions on observed composed resources +// so that downstream pipeline stages gated on readiness can advance. +func (p *DefaultDiffProcessor) RenderToStableState( ctx context.Context, xr *cmp.Unstructured, comp *apiextensionsv1.Composition, fns []pkgv1.Function, resourceID string, -) (render.Outputs, error) { - // Start with environment configs as baseline extra resources - var renderResources []un.Unstructured - - // Track resources we've already discovered to detect when we're done - discoveredResourcesMap := make(map[string]bool) - - // Set up for iterative discovery - const maxIterations = 10 // Prevent infinite loops - var lastOutput render.Outputs - var lastRenderErr error - - // Iteratively discover and fetch resources until we have all requirements - for iteration := 0; iteration < maxIterations; iteration++ { - // Perform render to get requirements - output, renderErr := p.config.RenderFunc(ctx, p.config.Logger, render.Inputs{...}) - - // Process requirements and check if we need to continue iterating - // ... - } - - return lastOutput, lastRenderErr -} + observedResources []cpd.Unstructured, + synthesizeReady bool, +) (render.CompositionOutputs, error) ``` -This implementation pattern may be valuable to extract into a shared utility if other commands need similar requirements -discovery capabilities in the future. +The loop is bounded by `MaxRenderIterations` (configurable via `--max-iterations`). The default-mode termination +criterion is "no new requirements were discovered on this iteration"; the eventual-state termination criterion is "no +new composed resources appeared and the Ready set has not changed". Both modes share the same body, with the +`synthesizeReady` flag selecting between them. + +##### 9.5.6.3 Composed-Resource Namespace Handling + +Crossplane's render pipeline (`SetComposedResourceMetadata` upstream) propagates the XR's namespace onto every composed +resource without checking the resource's scope. For namespaced XRs that compose a mix of namespaced and cluster-scoped +resources, this would put a namespace on cluster-scoped resources that don't accept one. The diff tool compensates with +`removeNamespacesFromClusterScopedResources`: after rendering, it inspects each composed resource's CRD scope and +strips the namespace from those flagged `Cluster`. Once upstream gains scope-aware propagation, this workaround can be +removed. -#### 9.5.6.3 Client Abstraction Layer +##### 9.5.6.4 Client Abstraction Layer The Diff command required a more abstract client layer to facilitate testing and to properly separate concerns. This led to the development of the client interfaces and implementations described in previous sections. These abstractions could potentially be moved to a shared location for use by other commands that need similar capabilities. -#### 9.5.6.4 Build Challenges +#### 9.5.6.5 Build Challenges The unit tests for this command follow the existing patterns for Crossplane CLI commands, and there are some valuable e2e tests due to the level of interaction with a real cluster, but the Diff command adds an intermediary layer of @@ -926,34 +1255,35 @@ and/or any security implications. ## 10. Future Enhancements -Several potential enhancements could be made to the Diff command: +Several potential enhancements could be made to the Diff command. (Items previously listed here that have since shipped +— additional output formats, persona-targeted machine-readable output, `comp`'s namespace/resource filtering — have +been removed.) -1. **File Output**: Add support for writing diffs to a file -2. **Diff Against Composition Revisions**: Support diffing against a new version of a composition, allowing Composition - Developers to visualize the impact of composition changes on existing XRs when upgrading from one revision to another +1. **File Output**: Add a flag to write diffs to a named file rather than stdout. (The structured output formats already + make this straightforward via shell redirection.) +2. **Diff Against Composition Revisions**: Surface the existing `CompositionRevisionClient` through user-facing flags + so Composition Developers can visualise the impact of moving from one Composition Revision to another, without + first updating the Composition in the cluster. 3. **Diff Against Unreleased Components**: Support diffing against upgraded schemas or compositions that aren't yet - applied + applied to the cluster. 4. **Diff Provider Changes**: Support diffing changes to provider configurations to understand how they might affect - managed resources, particularly with regard to schema changes -5. **Selective Diff**: Allow diffing only specific resources or resource types -6. **Additional Output Formats**: (e.g. gnu .diff) for programmatic consumption -7. **Integration with Policy Tools**: Evaluate changes against compliance rules -8. **Web UI Visualization**: For complex differences -9. **Save/Export Diffs**: For review or documentation purposes -10. **Crank Refactoring**: Several commands have similar code for loading resources and/or initializing kubernetes + managed resources, particularly with regard to schema changes. +5. **Selective Diff**: Allow diffing only specific resources or resource types within an XR's composed tree. +6. **Integration with Policy Tools**: Evaluate the produced diffs against compliance rules (OPA, Kyverno, etc.). +7. **Web UI Visualization**: For complex differences. +8. **Save/Export Diffs**: For review or documentation purposes (could compose with item 1). +9. **Crank Refactoring**: Several commands have similar code for loading resources and/or initializing Kubernetes clients. It would be valuable to refactor this code into a shared package to reduce duplication and improve maintainability. -11. **Command-line Requirements**: Support providing requirements directly on the command line instead of querying the - cluster, reducing permission requirements for the End-User Developer persona -12. **Local Rendering Mode**: Support a mode where rendering is performed without accessing the cluster for all - dependency resolution, using only locally available information and command-line inputs. This has considerable +10. **Command-line Requirements**: Support providing requirements directly on the command line instead of querying the + cluster, reducing permission requirements for the End-User Developer persona. +11. **Local Rendering Mode**: Support a mode where rendering is performed without accessing the cluster for all + dependency resolution, using only locally available information and command-line inputs. This has considerable overlap with the existing functionality of `crossplane render`. -13. **Targeted Permission Sets**: Define more granular permission sets for specific diff operations, allowing for - least-privilege implementations in restricted environments -14. **Persona-specific Output Formats**: Provide different output formats optimized for each user persona (e.g., - machine-readable JSON for CI, verbose explanations for composition developers, simplified views for end users) -15. **Namespace Flag Implementation**: The `--namespace` flag is currently defined but unused. Future implementation - could use it as a default namespace for XRs that don't specify one, or for looking up cluster resources +12. **Targeted Permission Sets**: Define more granular permission sets for specific diff operations, allowing for + least-privilege implementations in restricted environments. +13. **Function Container Reuse Across Invocations**: The current `CachedFunctionProvider` reuses containers across XRs + in a single run. A daemon-mode could reuse them across runs. These enhancements would expand the utility of the Diff command and make it more accessible to all user personas. @@ -975,24 +1305,37 @@ This tool is implemented as a standalone binary `crossplane-diff` with the follo ``` cmd/ ├── diff/ -│ ├── main.go # Main entry point and CLI structure -│ ├── diff.go # Core diff command implementation -│ ├── app_context.go # Application context and client initialization -│ ├── diffprocessor/ # Core diff processing logic -│ ├── client/ # Kubernetes and Crossplane client abstractions -│ │ ├── kubernetes/ # K8s API client implementations -│ │ └── crossplane/ # Crossplane-specific client implementations -│ └── renderer/ # Diff formatting and output rendering +│ ├── main.go # Top-level CLI: cli struct, global flags, kong wiring +│ ├── xr.go # `xr` subcommand (alias `diff`) +│ ├── comp.go # `comp` subcommand +│ ├── cmd_utils.go # Shared CommonCmdFields → ProcessorOption helpers +│ ├── app_context.go # AppContext: cluster client initialization +│ ├── diffprocessor/ # DiffProcessor, CompDiffProcessor, calculator, validator, +│ │ # resource manager, requirements provider, function provider +│ ├── client/ +│ │ ├── kubernetes/ # ApplyClient, ResourceClient, SchemaClient, TypeConverter +│ │ └── crossplane/ # Composition*, Definition, Environment, Function, +│ │ # Credential, ResourceTree clients +│ ├── renderer/ # DiffRenderer, CompDiffRenderer, structured (JSON/YAML) renderers +│ ├── ref/ # Composite-ref parsing for `comp --resource` +│ ├── kubecfg/ # kubeconfig resolution helpers +│ ├── types/ # Shared types (CompositionProvider, etc.) +│ ├── testutils/ # Mock builders, structured-assertion helpers used by tests +│ └── versioncmd/ # `version` subcommand ``` ### 12.2 Key Implementation Features The implementation includes several important features: -1. **Standalone Binary**: The tool is distributed as `crossplane-diff` as an independent utility -2. **Command Structure**: The command is `crossplane-diff diff [files...]` with subcommand architecture -3. **Crossplane v2 Support**: Full support for both v1 and v2 XRDs, including namespaced composite resources -4. **Enhanced Testing**: Comprehensive integration tests using `envtest` for faster, more reliable testing than full e2e tests +1. **Standalone Binary**: The tool is distributed as `crossplane-diff` as an independent utility. +2. **Two-Subcommand Architecture**: `xr` for diffing input XRs against the cluster; `comp` for impact analysis when + updating a composition. Both share the same per-XR rendering and diffing core via dependency injection. +3. **Crossplane v2 Support**: Full support for both v1 and v2 XRDs, including namespaced composite resources. +4. **Structured Output**: First-class JSON and YAML output (`--output {json,yaml}`) for CI/CD consumption, with errors + emitted to both stderr and the structured payload. +5. **Enhanced Testing**: Integration tests using `envtest` (much faster than full e2e), plus a structured-assertion + harness in `testutils/` that supports field-level diff assertions without ANSI golden-file fragility. ### 12.3 Crossplane v2 Namespace Enhancements diff --git a/design/design-doc-cli-diff/clear-layered-architecture.mermaid b/design/design-doc-cli-diff/clear-layered-architecture.mermaid index 6c6f3597..4c58c353 100644 --- a/design/design-doc-cli-diff/clear-layered-architecture.mermaid +++ b/design/design-doc-cli-diff/clear-layered-architecture.mermaid @@ -5,14 +5,14 @@ flowchart TD subgraph "Application Layer (Orchestration)" APP["AppContext\n(Context & client initialization)"] - DPR["DiffProcessor\n(Main processing logic)"] + DPR["DiffProcessor / CompDiffProcessor\n(xr and comp orchestration)"] LOAD["Loader\n(Resource loading)"] end - + subgraph "Domain Layer (Business Logic)" - CALC["Diff & Resources\n(DiffCalculator, ResourceManager, RequirementsProvider)"] + CALC["Diff & Resources\n(DiffCalculator, ResourceManager, RequirementsProvider, FunctionProvider)"] VALD["Validation\n(SchemaValidator)"] - REND["Rendering\n(DiffRenderer, DiffFormatter)"] + REND["Rendering\n(DiffRenderer, CompDiffRenderer, structured renderers)"] RENFN["Render Function\n(Resource composition rendering)"] end diff --git a/design/design-doc-cli-diff/clear-layered-architecture.svg b/design/design-doc-cli-diff/clear-layered-architecture.svg index eb3481e3..f4511455 100644 --- a/design/design-doc-cli-diff/clear-layered-architecture.svg +++ b/design/design-doc-cli-diff/clear-layered-architecture.svg @@ -1 +1 @@ -

External Systems

Client Layer (Infrastructure)

Domain Layer (Business Logic)

Application Layer (Orchestration)

Command Layer (CLI)

Cmd\n(Handles command arguments, flags)

AppContext\n(Context & client initialization)

DiffProcessor\n(Main processing logic)

Loader\n(Resource loading)

Diff & Resources\n(DiffCalculator, ResourceManager, RequirementsProvider)

Validation\n(SchemaValidator)

Rendering\n(DiffRenderer, DiffFormatter)

Render Function\n(Resource composition rendering)

Kubernetes Clients\n(Apply, Resource, Schema, Type)

Crossplane Clients\n(Composition, Definition, Function, etc.)

Kubernetes API\n(Live Cluster)

\ No newline at end of file +

External Systems

Client Layer (Infrastructure)

Domain Layer (Business Logic)

Application Layer (Orchestration)

Command Layer (CLI)

Cmd
(Handles command arguments, flags)

AppContext
(Context & client initialization)

DiffProcessor / CompDiffProcessor
(xr and comp orchestration)

Loader
(Resource loading)

Diff & Resources
(DiffCalculator, ResourceManager, RequirementsProvider, FunctionProvider)

Validation
(SchemaValidator)

Rendering
(DiffRenderer, CompDiffRenderer, structured renderers)

Render Function
(Resource composition rendering)

Kubernetes Clients
(Apply, Resource, Schema, Type)

Crossplane Clients
(Composition, Definition, Function, etc.)

Kubernetes API
(Live Cluster)

\ No newline at end of file diff --git a/design/design-doc-cli-diff/client-architecture.mermaid b/design/design-doc-cli-diff/client-architecture.mermaid index 7869a18e..ce8b3e5e 100644 --- a/design/design-doc-cli-diff/client-architecture.mermaid +++ b/design/design-doc-cli-diff/client-architecture.mermaid @@ -65,12 +65,26 @@ classDiagram %% Crossplane Clients class XpClients { +Composition CompositionClient + +CompositionRevision CompositionRevisionClient +Definition DefinitionClient +Environment EnvironmentClient +Function FunctionClient + +Credential CredentialClient +ResourceTree ResourceTreeClient +Initialize(ctx, logger) error } + + class CompositionRevisionClient { + <> + +Initialize(ctx) error + +GetRevision(ctx, name) (*Revision, error) + +ListRevisions(ctx, compName) ([]*Revision, error) + } + + class CredentialClient { + <> + +ResolveCredentials(ctx, refs) ([]*Secret, error) + } class CompositionClient { <> @@ -132,7 +146,9 @@ classDiagram DefaultCompositionClient --> ResourceClient XpClients --> CompositionClient + XpClients --> CompositionRevisionClient XpClients --> DefinitionClient XpClients --> EnvironmentClient XpClients --> FunctionClient + XpClients --> CredentialClient XpClients --> ResourceTreeClient diff --git a/design/design-doc-cli-diff/client-architecture.svg b/design/design-doc-cli-diff/client-architecture.svg index 3fd997ba..6623350f 100644 --- a/design/design-doc-cli-diff/client-architecture.svg +++ b/design/design-doc-cli-diff/client-architecture.svg @@ -1 +1 @@ -

K8sClients

+Apply ApplyClient

+Resource ResourceClient

+Schema SchemaClient

+Type TypeConverter

«interface»

ApplyClient

+DryRunApply(ctx, obj)(*Unstructured, error)

DefaultApplyClient

-dynamicClient dynamic.Interface

-typeConverter TypeConverter

-logger logging.Logger

«interface»

ResourceClient

+GetResource(ctx, gvk, namespace, name)(*Unstructured, error)

+ListResources(ctx, gvk, namespace)([]*Unstructured, error)

+GetResourcesByLabel(ctx, namespace, gvk, sel)([]*Unstructured, error)

+GetAllResourcesByLabels(ctx, gvks, selectors)([]*Unstructured, error)

DefaultResourceClient

-dynamicClient dynamic.Interface

-discoveryClient discovery.DiscoveryInterface

-converter TypeConverter

-logger logging.Logger

«interface»

SchemaClient

+GetCRD(ctx, gvk)(*Unstructured, error)

+IsCRDRequired(ctx, gvk) : bool

+ValidateResource(ctx, resource) : error

DefaultSchemaClient

-dynamicClient dynamic.Interface

-typeConverter TypeConverter

-logger logging.Logger

-resourceTypeMap map[GVK]bool

-resourceMapMu sync.RWMutex

«interface»

TypeConverter

+GVKToGVR(ctx, gvk)(GVR, error)

+GetResourceNameForGVK(ctx, gvk)(string, error)

DefaultTypeConverter

-dynamicClient dynamic.Interface

-discoveryClient discovery.DiscoveryInterface

-logger logging.Logger

-gvkToGVRMap map[GVK]GVR

-gvkToGVRMutex sync.RWMutex

XpClients

+Composition CompositionClient

+Definition DefinitionClient

+Environment EnvironmentClient

+Function FunctionClient

+ResourceTree ResourceTreeClient

+Initialize(ctx, logger) : error

«interface»

CompositionClient

+Initialize(ctx) : error

+FindMatchingComposition(ctx, res)(*Composition, error)

+ListCompositions(ctx)([]*Composition, error)

+GetComposition(ctx, name)(*Composition, error)

DefaultCompositionClient

-resourceClient ResourceClient

-logger logging.Logger

-compositions map[string]*Composition

«interface»

DefinitionClient

+Initialize(ctx) : error

+GetXRDs(ctx)([]*Unstructured, error)

+GetXRDForClaim(ctx, gvk)(*Unstructured, error)

+GetXRDForXR(ctx, gvk)(*Unstructured, error)

«interface»

FunctionClient

+Initialize(ctx) : error

+GetFunctionsFromPipeline(comp)([]Function, error)

+ListFunctions(ctx)([]Function, error)

«interface»

ResourceTreeClient

+Initialize(ctx) : error

+GetResourceTree(ctx, root)(*Resource, error)

«interface»

EnvironmentClient

+Initialize(ctx) : error

+GetEnvironmentConfigs(ctx)([]*Unstructured, error)

+GetEnvironmentConfig(ctx, name)(*Unstructured, error)

\ No newline at end of file +

K8sClients

+Apply ApplyClient

+Resource ResourceClient

+Schema SchemaClient

+Type TypeConverter

«interface»

ApplyClient

+DryRunApply(ctx, obj)(*Unstructured, error)

DefaultApplyClient

-dynamicClient dynamic.Interface

-typeConverter TypeConverter

-logger logging.Logger

«interface»

ResourceClient

+GetResource(ctx, gvk, namespace, name)(*Unstructured, error)

+ListResources(ctx, gvk, namespace)([]*Unstructured, error)

+GetResourcesByLabel(ctx, namespace, gvk, sel)([]*Unstructured, error)

+GetAllResourcesByLabels(ctx, gvks, selectors)([]*Unstructured, error)

DefaultResourceClient

-dynamicClient dynamic.Interface

-discoveryClient discovery.DiscoveryInterface

-converter TypeConverter

-logger logging.Logger

«interface»

SchemaClient

+GetCRD(ctx, gvk)(*Unstructured, error)

+IsCRDRequired(ctx, gvk) : bool

+ValidateResource(ctx, resource) : error

DefaultSchemaClient

-dynamicClient dynamic.Interface

-typeConverter TypeConverter

-logger logging.Logger

-resourceTypeMap map[GVK]bool

-resourceMapMu sync.RWMutex

«interface»

TypeConverter

+GVKToGVR(ctx, gvk)(GVR, error)

+GetResourceNameForGVK(ctx, gvk)(string, error)

DefaultTypeConverter

-dynamicClient dynamic.Interface

-discoveryClient discovery.DiscoveryInterface

-logger logging.Logger

-gvkToGVRMap map[GVK]GVR

-gvkToGVRMutex sync.RWMutex

XpClients

+Composition CompositionClient

+CompositionRevision CompositionRevisionClient

+Definition DefinitionClient

+Environment EnvironmentClient

+Function FunctionClient

+Credential CredentialClient

+ResourceTree ResourceTreeClient

+Initialize(ctx, logger) : error

«interface»

CompositionRevisionClient

+Initialize(ctx) : error

+GetRevision(ctx, name)(*Revision, error)

+ListRevisions(ctx, compName)([]*Revision, error)

«interface»

CredentialClient

+ResolveCredentials(ctx, refs)([]*Secret, error)

«interface»

CompositionClient

+Initialize(ctx) : error

+FindMatchingComposition(ctx, res)(*Composition, error)

+ListCompositions(ctx)([]*Composition, error)

+GetComposition(ctx, name)(*Composition, error)

DefaultCompositionClient

-resourceClient ResourceClient

-logger logging.Logger

-compositions map[string]*Composition

«interface»

DefinitionClient

+Initialize(ctx) : error

+GetXRDs(ctx)([]*Unstructured, error)

+GetXRDForClaim(ctx, gvk)(*Unstructured, error)

+GetXRDForXR(ctx, gvk)(*Unstructured, error)

«interface»

FunctionClient

+Initialize(ctx) : error

+GetFunctionsFromPipeline(comp)([]Function, error)

+ListFunctions(ctx)([]Function, error)

«interface»

ResourceTreeClient

+Initialize(ctx) : error

+GetResourceTree(ctx, root)(*Resource, error)

«interface»

EnvironmentClient

+Initialize(ctx) : error

+GetEnvironmentConfigs(ctx)([]*Unstructured, error)

+GetEnvironmentConfig(ctx, name)(*Unstructured, error)

\ No newline at end of file diff --git a/design/design-doc-cli-diff/conceptual-layers.mermaid b/design/design-doc-cli-diff/conceptual-layers.mermaid index bd1a2598..c315c429 100644 --- a/design/design-doc-cli-diff/conceptual-layers.mermaid +++ b/design/design-doc-cli-diff/conceptual-layers.mermaid @@ -53,7 +53,7 @@ classDiagram ClientLayer --> ExternalSystems : interacts with note for CommandLayer "Key Components: Cmd, Help functions" - note for ApplicationLayer "Key Components: AppContext, DiffProcessor, Loader" - note for DomainLayer "Key Components: DiffCalculator, SchemaValidator, RequirementsProvider, ResourceManager, DiffRenderer, Render Function" - note for ClientLayer "Key Components: Kubernetes Clients (Apply, Resource, Schema, TypeConverter), Crossplane Clients (Composition, Definition, Function, etc.)" + note for ApplicationLayer "Key Components: AppContext, DiffProcessor, CompDiffProcessor, Loader" + note for DomainLayer "Key Components: DiffCalculator, SchemaValidator, RequirementsProvider, FunctionProvider, ResourceManager, DiffRenderer, CompDiffRenderer, Render Function" + note for ClientLayer "Key Components: Kubernetes Clients (Apply, Resource, Schema, TypeConverter), Crossplane Clients (Composition, CompositionRevision, Definition, Environment, Function, Credential, ResourceTree)" note for ExternalSystems "Kubernetes API running in a live cluster with Crossplane installed" diff --git a/design/design-doc-cli-diff/conceptual-layers.svg b/design/design-doc-cli-diff/conceptual-layers.svg index 461dc011..090d0225 100644 --- a/design/design-doc-cli-diff/conceptual-layers.svg +++ b/design/design-doc-cli-diff/conceptual-layers.svg @@ -1 +1 @@ -

uses

coordinates

depends on

interacts with

«layer»

CommandLayer

Responsibility: CLI entry point

- Command parsing

- Argument validation

- Help text generation

- Entry point coordination

«layer»

ApplicationLayer

Responsibility: Application orchestration

- Context management

- Client initialization

- Process coordination

- Resource loading

- Result aggregation

«layer»

DomainLayer

Responsibility: Business logic

- Diff calculation

- Resource rendering

- Resource management

- Schema validation

- Requirement processing

- Diff visualization

«layer»

ClientLayer

Responsibility: Infrastructure access

- Kubernetes API interaction

- Crossplane resource access

- Resource conversion

- Type handling

- Server-side apply

«layer»

ExternalSystems

Responsibility: External services

- Kubernetes API server

- Resources in cluster

- CRDs and schemas

Key Components: Cmd, Help functions

Key Components: AppContext, DiffProcessor, Loader

Key Components: DiffCalculator, SchemaValidator, RequirementsProvider, ResourceManager, DiffRenderer, Render Function

Key Components: Kubernetes Clients (Apply, Resource, Schema, TypeConverter), Crossplane Clients (Composition, Definition, Function, etc.)

Kubernetes API running in a live cluster with Crossplane installed

\ No newline at end of file +

uses

coordinates

depends on

interacts with

«layer»

CommandLayer

Responsibility: CLI entry point

- Command parsing

- Argument validation

- Help text generation

- Entry point coordination

«layer»

ApplicationLayer

Responsibility: Application orchestration

- Context management

- Client initialization

- Process coordination

- Resource loading

- Result aggregation

«layer»

DomainLayer

Responsibility: Business logic

- Diff calculation

- Resource rendering

- Resource management

- Schema validation

- Requirement processing

- Diff visualization

«layer»

ClientLayer

Responsibility: Infrastructure access

- Kubernetes API interaction

- Crossplane resource access

- Resource conversion

- Type handling

- Server-side apply

«layer»

ExternalSystems

Responsibility: External services

- Kubernetes API server

- Resources in cluster

- CRDs and schemas

Key Components: Cmd, Help functions

Key Components: AppContext, DiffProcessor, CompDiffProcessor, Loader

Key Components: DiffCalculator, SchemaValidator, RequirementsProvider, FunctionProvider, ResourceManager, DiffRenderer, CompDiffRenderer, Render Function

Key Components: Kubernetes Clients (Apply, Resource, Schema, TypeConverter), Crossplane Clients (Composition, CompositionRevision, Definition, Environment, Function, Credential, ResourceTree)

Kubernetes API running in a live cluster with Crossplane installed

\ No newline at end of file diff --git a/design/design-doc-cli-diff/diff-call-sequence.mermaid b/design/design-doc-cli-diff/diff-call-sequence.mermaid index 34d580e0..5b206a96 100644 --- a/design/design-doc-cli-diff/diff-call-sequence.mermaid +++ b/design/design-doc-cli-diff/diff-call-sequence.mermaid @@ -1,62 +1,91 @@ sequenceDiagram participant CLI as CLI Entry Point - participant Cmd as diff.Cmd + participant Cmd as XRCmd / CompCmd participant Loader as CompositeLoader + participant CDP as DefaultCompDiffProcessor participant DP as DefaultDiffProcessor participant CompClient as CompositionClient + participant FnProv as FunctionProvider participant Render as Render Function participant ReqProv as RequirementsProvider + participant ResMgr as ResourceManager + participant Validator as SchemaValidator participant DiffCalc as DiffCalculator participant K8sAPI as Kubernetes API - participant DiffRend as DiffRenderer - + participant Renderer as DiffRenderer / CompDiffRenderer + CLI->>Cmd: Run() Cmd->>Loader: Load() - Loader-->>Cmd: []*unstructured.Unstructured - - Cmd->>DP: Initialize(ctx) - DP->>DP: Load CRDs and Resources - - Cmd->>DP: PerformDiff(ctx, stdout, resources) - - loop For each resource - DP->>DP: SanitizeXR(res) - DP->>CompClient: FindMatchingComposition(ctx, res) - CompClient-->>DP: *apiextensionsv1.Composition - - DP->>DP: RenderWithRequirements(ctx, xr, comp, fns) - - loop Until no new requirements - DP->>Render: Render(ctx, inputs) - Render-->>DP: outputs with requirements - - alt Has Requirements - DP->>ReqProv: ProvideRequirements(ctx, requirements) - ReqProv->>K8sAPI: GetResource/GetResourcesByLabel - K8sAPI-->>ReqProv: Resources - ReqProv-->>DP: Additional Resources + Loader-->>Cmd: []*Unstructured + + Note over Cmd, CDP: Outer setup differs by subcommand. The per-XR body is identical. + alt comp subcommand + Cmd->>CDP: Initialize(ctx) + Cmd->>CDP: DiffComposition(ctx, comps, ns, resources) + CDP->>CompClient: ListXRsUsingComposition(ctx, comp, ns, resources) + CompClient-->>CDP: [affected XRs] + CDP->>DiffCalc: CalculateDiff(ctx, currentComp, newComp) + DiffCalc-->>CDP: composition-level diff + Note over CDP, DP: For each affected XR, CDP calls DP.DiffSingleResource. See per-XR body below. + else xr subcommand + Cmd->>DP: Initialize(ctx) + Cmd->>DP: PerformDiff(ctx, resources, clusterCompositionProvider) + Note over DP: PerformDiff iterates the input resources, calling DiffSingleResource on each. See per-XR body below. + end + + rect rgb(245, 245, 255) + Note over DP, DiffCalc: per-XR body. DiffSingleResource(ctx, xr, compositionProvider). Driven once per input XR (xr) or once per affected XR (comp). + loop For each XR being diffed + DP->>DP: SanitizeXR(res) + DP->>CompClient: compositionProvider(ctx, res) + CompClient-->>DP: *Composition (cluster's, or proposed under comp) + DP->>FnProv: GetFunctionsForComposition(comp) + FnProv-->>DP: []Function + DP->>ResMgr: FetchCurrentObject(ctx, xr) / FetchObservedResources(ctx, xr) + ResMgr-->>DP: existing XR + observed composed resources + + DP->>DP: RenderToStableState(ctx, xr, comp, fns, id, observed, eventualState) + loop Until requirements stabilise (or steady state under --eventual-state) + DP->>Render: Render(ctx, inputs) + Render-->>DP: CompositionOutputs (incl. RequiredResources) + alt Has unresolved RequiredResources + DP->>ReqProv: ResolveSelectors(ctx, selectors, ns) + ReqProv->>K8sAPI: GetResource / GetResourcesByLabel + K8sAPI-->>ReqProv: resources + ReqProv-->>DP: additional inputs + end end - end - DP->>DP: propagateNamespacesToManagedResources(ctx, xr, composed) - DP->>DP: ValidateResources(ctx, xr, composed) - - DP->>DiffCalc: CalculateDiffs(ctx, xr, desired) - - loop For each rendered resource - DiffCalc->>K8sAPI: FetchCurrentObject - K8sAPI-->>DiffCalc: Current Object - DiffCalc->>K8sAPI: DryRunApply(desired) - K8sAPI-->>DiffCalc: Result - DiffCalc->>DiffCalc: GenerateDiffWithOptions() + DP->>DP: removeNamespacesFromClusterScopedResources(...) + opt Nested XRs present + DP->>DP: Recurse via diffSingleResourceInternal (subject to --max-nested-depth) + end + DP->>Validator: ValidateResources / ValidateScopeConstraints + Validator-->>DP: ok / err + + DP->>DiffCalc: CalculateNonRemovalDiffs(ctx, xr, parent, desired) + loop For each rendered resource + DiffCalc->>ResMgr: FetchCurrentObject + ResMgr->>K8sAPI: GET + K8sAPI-->>ResMgr: current + DiffCalc->>K8sAPI: DryRunApply(desired) + K8sAPI-->>DiffCalc: result + end + DiffCalc-->>DP: diffs + rendered keys + + DP->>DiffCalc: CalculateRemovedResourceDiffs(ctx, xr, renderedKeys) + DiffCalc->>ResMgr: walk live tree + DiffCalc-->>DP: removal diffs end - - DiffCalc-->>DP: map[string]*ResourceDiff end - - DP->>DiffRend: RenderDiffs(stdout, allDiffs) - DiffRend->>DiffRend: FormatDiff(diffs, options) - DiffRend-->>DP: Formatted Output - - DP-->>Cmd: Success/Error - Cmd-->>CLI: Return Code + + alt comp subcommand + CDP->>Renderer: RenderCompDiff(output) + CDP->>DP: Cleanup(ctx) + else xr subcommand + DP->>Renderer: RenderDiffs(allDiffs, errs) + DP->>FnProv: Cleanup(ctx) + end + + Renderer-->>Cmd: formatted output + Cmd-->>CLI: ExitCode diff --git a/design/design-doc-cli-diff/diff-call-sequence.svg b/design/design-doc-cli-diff/diff-call-sequence.svg index 962219f3..92d3698a 100644 --- a/design/design-doc-cli-diff/diff-call-sequence.svg +++ b/design/design-doc-cli-diff/diff-call-sequence.svg @@ -1 +1 @@ -DiffRendererKubernetes APIDiffCalculatorRequirementsProviderRender FunctionCompositionClientDefaultDiffProcessorCompositeLoaderdiff.CmdCLI Entry PointDiffRendererKubernetes APIDiffCalculatorRequirementsProviderRender FunctionCompositionClientDefaultDiffProcessorCompositeLoaderdiff.CmdCLI Entry Pointalt[Has Requirements]loop[Until no new requirements]loop[For each rendered resource]loop[For each resource]Run()Load()[]*unstructured.UnstructuredInitialize(ctx)Load CRDs and ResourcesPerformDiff(ctx, stdout, resources)SanitizeXR(res)FindMatchingComposition(ctx, res)*apiextensionsv1.CompositionRenderWithRequirements(ctx, xr, comp, fns)Render(ctx, inputs)outputs with requirementsProvideRequirements(ctx, requirements)GetResource/GetResourcesByLabelResourcesAdditional ResourcespropagateNamespacesToManagedResources(ctx, xr, composed)ValidateResources(ctx, xr, composed)CalculateDiffs(ctx, xr, desired)FetchCurrentObjectCurrent ObjectDryRunApply(desired)ResultGenerateDiffWithOptions()map[string]*ResourceDiffRenderDiffs(stdout, allDiffs)FormatDiff(diffs, options)Formatted OutputSuccess/ErrorReturn Code \ No newline at end of file +DiffRenderer / CompDiffRendererKubernetes APIDiffCalculatorSchemaValidatorResourceManagerRequirementsProviderRender FunctionFunctionProviderCompositionClientDefaultDiffProcessorDefaultCompDiffProcessorCompositeLoaderXRCmd / CompCmdCLI Entry PointDiffRenderer / CompDiffRendererKubernetes APIDiffCalculatorSchemaValidatorResourceManagerRequirementsProviderRender FunctionFunctionProviderCompositionClientDefaultDiffProcessorDefaultCompDiffProcessorCompositeLoaderXRCmd / CompCmdCLI Entry PointOuter setup differs by subcommand. The per-XR body is identical.For each affected XR, CDP calls DP.DiffSingleResource. See per-XR body below.PerformDiff iterates the input resources, calling DiffSingleResource on each. See per-XR body below.alt[comp subcommand][xr subcommand]per-XR body. DiffSingleResource(ctx, xr, compositionProvider). Driven once per input XR (xr) or once per affected XR (comp).alt[Has unresolved RequiredResources]loop[Until requirements stabilise (or steady state under --eventual-state)]opt[Nested XRs present]loop[For each rendered resource]loop[For each XR being diffed]alt[comp subcommand][xr subcommand]Run()Load()[]*UnstructuredInitialize(ctx)DiffComposition(ctx, comps, ns, resources)ListXRsUsingComposition(ctx, comp, ns, resources)[affected XRs]CalculateDiff(ctx, currentComp, newComp)composition-level diffInitialize(ctx)PerformDiff(ctx, resources, clusterCompositionProvider)SanitizeXR(res)compositionProvider(ctx, res)*Composition (cluster's, or proposed under comp)GetFunctionsForComposition(comp)[]FunctionFetchCurrentObject(ctx, xr) / FetchObservedResources(ctx, xr)existing XR + observed composed resourcesRenderToStableState(ctx, xr, comp, fns, id, observed, eventualState)Render(ctx, inputs)CompositionOutputs (incl. RequiredResources)ResolveSelectors(ctx, selectors, ns)GetResource / GetResourcesByLabelresourcesadditional inputsremoveNamespacesFromClusterScopedResources(...)Recurse via diffSingleResourceInternal (subject to --max-nested-depth)ValidateResources / ValidateScopeConstraintsok / errCalculateNonRemovalDiffs(ctx, xr, parent, desired)FetchCurrentObjectGETcurrentDryRunApply(desired)resultdiffs + rendered keysCalculateRemovedResourceDiffs(ctx, xr, renderedKeys)walk live treeremoval diffsRenderCompDiff(output)Cleanup(ctx)RenderDiffs(allDiffs, errs)Cleanup(ctx)formatted outputExitCode \ No newline at end of file diff --git a/design/design-doc-cli-diff/diff-data-flow.svg b/design/design-doc-cli-diff/diff-data-flow.svg index 6520cae8..724063a0 100644 --- a/design/design-doc-cli-diff/diff-data-flow.svg +++ b/design/design-doc-cli-diff/diff-data-flow.svg @@ -1 +1 @@ -

Output

External Systems

Processing

Input

For each XR

Discover Requirements

Additional Resources

Get Current State

DryRun Apply

Fetch Resources

YAML Files/StdIn

Loader

Unstructured Resources

DiffProcessor

Find Matching Composition

Render Resources\nwith Functions

RequirementsProvider

Validate Resources

Calculate Diffs

Kubernetes API

Format Diffs

Render to Console

\ No newline at end of file +

Output

External Systems

Processing

Input

For each XR

Discover Requirements

Additional Resources

Get Current State

DryRun Apply

Fetch Resources

YAML Files/StdIn

Loader

Unstructured Resources

DiffProcessor

Find Matching Composition

Render Resources
with Functions

RequirementsProvider

Validate Resources

Calculate Diffs

Kubernetes API

Format Diffs

Render to Console

\ No newline at end of file diff --git a/design/design-doc-cli-diff/diff-processor-architecture.mermaid b/design/design-doc-cli-diff/diff-processor-architecture.mermaid index 10c2f4c2..54e132dd 100644 --- a/design/design-doc-cli-diff/diff-processor-architecture.mermaid +++ b/design/design-doc-cli-diff/diff-processor-architecture.mermaid @@ -1,119 +1,137 @@ classDiagram class DiffProcessor { <> - +PerformDiff(ctx, stdout, resources) error + +PerformDiff(ctx, resources, compositionProvider) (bool, error) + +DiffSingleResource(ctx, res, compositionProvider) (map, error) +Initialize(ctx) error + +Cleanup(ctx) error } - + + class CompDiffProcessor { + <> + +DiffComposition(ctx, comps, namespace, resources) (bool, error) + +Initialize(ctx) error + +Cleanup(ctx) error + } + class DefaultDiffProcessor { - -fnClient FunctionClient + -fnProvider FunctionProvider -compClient CompositionClient + -defClient DefinitionClient + -schemaClient SchemaClient + -treeClient ResourceTreeClient + -applyClient ApplyClient -config ProcessorConfig -schemaValidator SchemaValidator -diffCalculator DiffCalculator -diffRenderer DiffRenderer -requirementsProvider RequirementsProvider +Initialize(ctx) error - +PerformDiff(ctx, stdout, resources) error - -DiffSingleResource(ctx, res) error - -SanitizeXR(res, resourceID) error - -RenderWithRequirements(ctx, xr, comp, fns, resourceID) error + +Cleanup(ctx) error + +PerformDiff(ctx, resources, compositionProvider) (bool, error) + +DiffSingleResource(ctx, res, compositionProvider) (map, error) + +RenderToStableState(ctx, xr, comp, fns, id, observed, synthReady) (Outputs, error) + } + + class DefaultCompDiffProcessor { + -compositionClient CompositionClient + -config ProcessorConfig + -xrProc DiffProcessor + -compDiffRenderer CompDiffRenderer + +DiffComposition(ctx, comps, ns, resources) (bool, error) } - + class ProcessorConfig { +Namespace string +Colorize bool +Compact bool + +OutputFormat OutputFormat + +MaxNestedDepth int + +MaxRenderIterations int + +IncludeManual bool + +EventualState bool + +IgnorePaths string slice + +FunctionCredentials Secret slice + +FunctionRegistryOverride string + +CrossplaneRenderBinary string + +Stdout io.Writer + +Stderr io.Writer +Logger logging.Logger - +RenderFunc RenderFunc + +RenderFunc RenderFn +Factories ComponentFactories - +GetDiffOptions() DiffOptions - +SetDefaultFactories() } - + class ComponentFactories { - +ResourceManager func(client, logger) - +SchemaValidator func(schema, def, logger) - +DiffCalculator func(apply, tree, rm, logger, opts) - +DiffRenderer func(logger, opts) - +RequirementsProvider func(res, def, render, logger) + +ResourceManager + +SchemaValidator + +DiffCalculator + +DiffRenderer + +CompDiffRenderer + +RequirementsProvider + +FunctionProvider } - + class SchemaValidator { <> +ValidateResources(ctx, xr, composed) error +EnsureComposedResourceCRDs(ctx, resources) error + +ValidateScopeConstraints(ctx, resource, ns, isClaimRoot) error } - - class DefaultSchemaValidator { - -defClient DefinitionClient - -schemaClient SchemaClient - -logger logging.Logger - -crds []*CustomResourceDefinition - +LoadCRDs(ctx) error - +SetCRDs(crds) - +GetCRDs() []*CustomResourceDefinition - } - + class DiffCalculator { <> - +CalculateDiff(ctx, composite, desired) error - +CalculateDiffs(ctx, xr, desired) error - +CalculateRemovedResourceDiffs(ctx, xr, rendered) error + +CalculateDiff(ctx, composite, desired) (Diff, error) + +CalculateDiffs(ctx, xr, desired) (map, error) + +CalculateNonRemovalDiffs(ctx, xr, parent, desired) (map, set, error) + +CalculateRemovedResourceDiffs(ctx, xr, rendered) (map, error) } - - class DefaultDiffCalculator { - -treeClient ResourceTreeClient - -applyClient ApplyClient - -resourceManager ResourceManager - -logger logging.Logger - -diffOptions DiffOptions - +SetDiffOptions(options) - } - + class ResourceManager { <> - +FetchCurrentObject(ctx, composite, desired) error - +UpdateOwnerRefs(parent, child) + +FetchCurrentObject(ctx, composite, desired) (obj, bool, error) + +UpdateOwnerRefs(ctx, parent, child) + +FetchObservedResources(ctx, xr) (resources, error) } - - class DefaultResourceManager { - -client ResourceClient - -logger logging.Logger - -createResourceID(gvk, namespace, name, generateName) string - -lookupByComposite(ctx, composite, desired) error + + class FunctionProvider { + <> + +GetFunctionsForComposition(comp) (functions, error) + +Cleanup(ctx) error } - + class RequirementsProvider { +client ResourceClient +envClient EnvironmentClient +renderFn RenderFunc +logger logging.Logger - +resourceCache map[string]*Unstructured + +resourceCache map +Initialize(ctx) error - +ProvideRequirements(ctx, requirements) error - +getCachedResource(apiVersion, kind, name) - +cacheResources(resources) + +ResolveSelectors(ctx, selectors, ns) error +ClearCache() } - + class DiffRenderer { <> - +RenderDiffs(stdout, diffs) error + +RenderDiffs(diffs, errs) error + } + + class CompDiffRenderer { + <> + +RenderCompDiff(output) error } - + DefaultDiffProcessor ..|> DiffProcessor + DefaultCompDiffProcessor ..|> CompDiffProcessor + DefaultCompDiffProcessor --> DiffProcessor : embeds (xrProc) DefaultDiffProcessor --> ProcessorConfig DefaultDiffProcessor --> SchemaValidator DefaultDiffProcessor --> DiffCalculator DefaultDiffProcessor --> ResourceManager DefaultDiffProcessor --> RequirementsProvider + DefaultDiffProcessor --> FunctionProvider DefaultDiffProcessor --> DiffRenderer - + DefaultCompDiffProcessor --> CompDiffRenderer + ProcessorConfig --> ComponentFactories - - DefaultSchemaValidator ..|> SchemaValidator - DefaultDiffCalculator ..|> DiffCalculator - DefaultResourceManager ..|> ResourceManager - - DefaultDiffCalculator --> ResourceManager + + DiffCalculator --> ResourceManager diff --git a/design/design-doc-cli-diff/diff-processor-architecture.svg b/design/design-doc-cli-diff/diff-processor-architecture.svg index 6bf14829..59b22e04 100644 --- a/design/design-doc-cli-diff/diff-processor-architecture.svg +++ b/design/design-doc-cli-diff/diff-processor-architecture.svg @@ -1 +1 @@ -

«interface»

DiffProcessor

+PerformDiff(ctx, stdout, resources) : error

+Initialize(ctx) : error

DefaultDiffProcessor

-fnClient FunctionClient

-compClient CompositionClient

-config ProcessorConfig

-schemaValidator SchemaValidator

-diffCalculator DiffCalculator

-diffRenderer DiffRenderer

-requirementsProvider RequirementsProvider

+Initialize(ctx) : error

+PerformDiff(ctx, stdout, resources) : error

-DiffSingleResource(ctx, res) : error

-SanitizeXR(res, resourceID) : error

-RenderWithRequirements(ctx, xr, comp, fns, resourceID) : error

ProcessorConfig

+Namespace string

+Colorize bool

+Compact bool

+Logger logging.Logger

+RenderFunc RenderFunc

+Factories ComponentFactories

+GetDiffOptions() : DiffOptions

+SetDefaultFactories()

ComponentFactories

+ResourceManager func(client, logger)

+SchemaValidator func(schema, def, logger)

+DiffCalculator func(apply, tree, rm, logger, opts)

+DiffRenderer func(logger, opts)

+RequirementsProvider func(res, def, render, logger)

«interface»

SchemaValidator

+ValidateResources(ctx, xr, composed) : error

+EnsureComposedResourceCRDs(ctx, resources) : error

DefaultSchemaValidator

-defClient DefinitionClient

-schemaClient SchemaClient

-logger logging.Logger

-crds []*CustomResourceDefinition

+LoadCRDs(ctx) : error

+SetCRDs(crds)

+GetCRDs() : []*CustomResourceDefinition

«interface»

DiffCalculator

+CalculateDiff(ctx, composite, desired) : error

+CalculateDiffs(ctx, xr, desired) : error

+CalculateRemovedResourceDiffs(ctx, xr, rendered) : error

DefaultDiffCalculator

-treeClient ResourceTreeClient

-applyClient ApplyClient

-resourceManager ResourceManager

-logger logging.Logger

-diffOptions DiffOptions

+SetDiffOptions(options)

«interface»

ResourceManager

+FetchCurrentObject(ctx, composite, desired) : error

+UpdateOwnerRefs(parent, child)

DefaultResourceManager

-client ResourceClient

-logger logging.Logger

-createResourceID(gvk, namespace, name, generateName) : string

-lookupByComposite(ctx, composite, desired) : error

RequirementsProvider

+client ResourceClient

+envClient EnvironmentClient

+renderFn RenderFunc

+logger logging.Logger

+resourceCache map[string]*Unstructured

+Initialize(ctx) : error

+ProvideRequirements(ctx, requirements) : error

+getCachedResource(apiVersion, kind, name)

+cacheResources(resources)

+ClearCache()

«interface»

DiffRenderer

+RenderDiffs(stdout, diffs) : error

\ No newline at end of file +

embeds (xrProc)

«interface»

DiffProcessor

+PerformDiff(ctx, resources, compositionProvider)(bool, error)

+DiffSingleResource(ctx, res, compositionProvider)(map, error)

+Initialize(ctx) : error

+Cleanup(ctx) : error

«interface»

CompDiffProcessor

+DiffComposition(ctx, comps, namespace, resources)(bool, error)

+Initialize(ctx) : error

+Cleanup(ctx) : error

DefaultDiffProcessor

-fnProvider FunctionProvider

-compClient CompositionClient

-defClient DefinitionClient

-schemaClient SchemaClient

-treeClient ResourceTreeClient

-applyClient ApplyClient

-config ProcessorConfig

-schemaValidator SchemaValidator

-diffCalculator DiffCalculator

-diffRenderer DiffRenderer

-requirementsProvider RequirementsProvider

+Initialize(ctx) : error

+Cleanup(ctx) : error

+PerformDiff(ctx, resources, compositionProvider)(bool, error)

+DiffSingleResource(ctx, res, compositionProvider)(map, error)

+RenderToStableState(ctx, xr, comp, fns, id, observed, synthReady)(Outputs, error)

DefaultCompDiffProcessor

-compositionClient CompositionClient

-config ProcessorConfig

-xrProc DiffProcessor

-compDiffRenderer CompDiffRenderer

+DiffComposition(ctx, comps, ns, resources)(bool, error)

ProcessorConfig

+Namespace string

+Colorize bool

+Compact bool

+OutputFormat OutputFormat

+MaxNestedDepth int

+MaxRenderIterations int

+IncludeManual bool

+EventualState bool

+IgnorePaths string slice

+FunctionCredentials Secret slice

+FunctionRegistryOverride string

+CrossplaneRenderBinary string

+Stdout io.Writer

+Stderr io.Writer

+Logger logging.Logger

+RenderFunc RenderFn

+Factories ComponentFactories

ComponentFactories

+ResourceManager

+SchemaValidator

+DiffCalculator

+DiffRenderer

+CompDiffRenderer

+RequirementsProvider

+FunctionProvider

«interface»

SchemaValidator

+ValidateResources(ctx, xr, composed) : error

+EnsureComposedResourceCRDs(ctx, resources) : error

+ValidateScopeConstraints(ctx, resource, ns, isClaimRoot) : error

«interface»

DiffCalculator

+CalculateDiff(ctx, composite, desired)(Diff, error)

+CalculateDiffs(ctx, xr, desired)(map, error)

+CalculateNonRemovalDiffs(ctx, xr, parent, desired)(map, set, error)

+CalculateRemovedResourceDiffs(ctx, xr, rendered)(map, error)

«interface»

ResourceManager

+FetchCurrentObject(ctx, composite, desired)(obj, bool, error)

+UpdateOwnerRefs(ctx, parent, child)

+FetchObservedResources(ctx, xr)(resources, error)

«interface»

FunctionProvider

+GetFunctionsForComposition(comp)(functions, error)

+Cleanup(ctx) : error

RequirementsProvider

+client ResourceClient

+envClient EnvironmentClient

+renderFn RenderFunc

+logger logging.Logger

+resourceCache map

+Initialize(ctx) : error

+ResolveSelectors(ctx, selectors, ns) : error

+ClearCache()

«interface»

DiffRenderer

+RenderDiffs(diffs, errs) : error

«interface»

CompDiffRenderer

+RenderCompDiff(output) : error

\ No newline at end of file diff --git a/design/design-doc-cli-diff/diff-rendering-architecture.mermaid b/design/design-doc-cli-diff/diff-rendering-architecture.mermaid index d399bba0..670e3d64 100644 --- a/design/design-doc-cli-diff/diff-rendering-architecture.mermaid +++ b/design/design-doc-cli-diff/diff-rendering-architecture.mermaid @@ -1,16 +1,51 @@ classDiagram class DiffRenderer { <> - +RenderDiffs(stdout, diffs) error + +RenderDiffs(diffs, errs) error } - + + class CompDiffRenderer { + <> + +RenderCompDiff(output) error + } + class DefaultDiffRenderer { -logger logging.Logger -diffOpts DiffOptions - +SetDiffOptions(options) - +RenderDiffs(stdout, diffs) error + -stdout io.Writer + +RenderDiffs(diffs, errs) error + } + + class StructuredDiffRenderer { + -logger logging.Logger + -format OutputFormat + -stdout io.Writer + -stderr io.Writer + +RenderDiffs(diffs, errs) error + } + + class DefaultCompDiffRenderer { + -diffRenderer DiffRenderer + -logger logging.Logger + -diffOpts DiffOptions + +RenderCompDiff(output) error + } + + class StructuredCompDiffRenderer { + -logger logging.Logger + -format OutputFormat + -stdout io.Writer + -stderr io.Writer + +RenderCompDiff(output) error + } + + class OutputFormat { + <> + OutputFormatDiff + OutputFormatJSON + OutputFormatYAML } - + class DiffOptions { +UseColors bool +AddPrefix string @@ -19,32 +54,88 @@ classDiagram +ContextLines int +ChunkSeparator string +Compact bool + +OutputFormat OutputFormat } - - class DiffFormatter { - <> - +Format(diffs, options) string + + class StructuredDiffOutput { + +Diffs map + +Summary Summary + +Errors OutputError slice + } + + class CompDiffOutput { + +CompositionDiff *CompositionDiff + +AffectedResources AffectedResourcesSummary + +XRImpacts []XRImpact + +Errors []OutputError + } + + class CompositionDiff { + +Name string + +Diff *ResourceDiff + } + + class XRImpact { + +Kind string + +Name string + +Namespace string + +Status XRStatus + +Downstream DownstreamChanges + } + + class XRStatus { + <> + XRStatusChanged + XRStatusUnchanged + XRStatusErrored + } + + class AffectedResourcesSummary { + +Total int + +Changed int + +Unchanged int + +Errored int + } + + class DownstreamChanges { + +Added int + +Modified int + +Removed int + +Resources []DiffEntry + } + + class OutputError { + +ResourceID string + +Message string + +ValidationFailures []ResourceValidationFailure } - - class FullDiffFormatter { - +Format(diffs, options) string + + class ResourceValidationFailure { + +APIVersion string + +Kind string + +Name string + +Namespace string + +Status string + +Errors []FieldValidationError } - - class CompactDiffFormatter { - +Format(diffs, options) string - -stringBlocksWithContext(changes, lines, opts) string + + class FieldValidationError { + +Type string + +Field string + +Message string + +Value any } - + class ResourceDiff { +Gvk GroupVersionKind +ResourceName string +DiffType DiffType - +LineDiffs []diffmatchpatch.Diff - +Current *Unstructured - +Desired *Unstructured + +LineDiffs []diff + +Current Unstructured + +Desired Unstructured +GetDiffKey() string } - + class DiffType { <> DiffTypeAdded @@ -52,47 +143,26 @@ classDiagram DiffTypeModified DiffTypeEqual } - + DefaultDiffRenderer ..|> DiffRenderer + StructuredDiffRenderer ..|> DiffRenderer + DefaultCompDiffRenderer ..|> CompDiffRenderer + StructuredCompDiffRenderer ..|> CompDiffRenderer + DefaultDiffRenderer --> DiffOptions DefaultDiffRenderer ..> ResourceDiff - - FullDiffFormatter ..|> DiffFormatter - CompactDiffFormatter ..|> DiffFormatter - + StructuredDiffRenderer --> StructuredDiffOutput + StructuredCompDiffRenderer --> CompDiffOutput + DefaultCompDiffRenderer --> DiffRenderer + + StructuredDiffOutput --> OutputError + CompDiffOutput --> CompositionDiff + CompDiffOutput --> XRImpact + CompDiffOutput --> AffectedResourcesSummary + CompDiffOutput --> OutputError + OutputError --> ResourceValidationFailure + ResourceValidationFailure --> FieldValidationError + XRImpact --> XRStatus + XRImpact --> DownstreamChanges + ResourceDiff --> DiffType - - %% Functions - class GenerateDiffWithOptions { - <> - +GenerateDiffWithOptions(current, desired, logger, options) (*ResourceDiff, error) - } - - class FormatDiff { - <> - +FormatDiff(diffs, options) string - } - - class GetLineDiff { - <> - +GetLineDiff(oldText, newText) []diffmatchpatch.Diff - } - - class NewFormatter { - <> - +NewFormatter(compact) DiffFormatter - } - - class MakeDiffKey { - <> - +MakeDiffKey(apiVersion, kind, name) string - } - - FormatDiff --> NewFormatter - NewFormatter --> FullDiffFormatter - NewFormatter --> CompactDiffFormatter - - GenerateDiffWithOptions --> GetLineDiff - GenerateDiffWithOptions --> ResourceDiff - - DefaultDiffRenderer --> FormatDiff diff --git a/design/design-doc-cli-diff/diff-rendering-architecture.svg b/design/design-doc-cli-diff/diff-rendering-architecture.svg index 741e3dc6..3509b805 100644 --- a/design/design-doc-cli-diff/diff-rendering-architecture.svg +++ b/design/design-doc-cli-diff/diff-rendering-architecture.svg @@ -1 +1 @@ -

«interface»

DiffRenderer

+RenderDiffs(stdout, diffs) : error

DefaultDiffRenderer

-logger logging.Logger

-diffOpts DiffOptions

+SetDiffOptions(options)

+RenderDiffs(stdout, diffs) : error

DiffOptions

+UseColors bool

+AddPrefix string

+DeletePrefix string

+ContextPrefix string

+ContextLines int

+ChunkSeparator string

+Compact bool

«interface»

DiffFormatter

+Format(diffs, options) : string

FullDiffFormatter

+Format(diffs, options) : string

CompactDiffFormatter

+Format(diffs, options) : string

-stringBlocksWithContext(changes, lines, opts) : string

ResourceDiff

+Gvk GroupVersionKind

+ResourceName string

+DiffType DiffType

+LineDiffs []diffmatchpatch.Diff

+Current *Unstructured

+Desired *Unstructured

+GetDiffKey() : string

«enumeration»

DiffType

DiffTypeAdded

DiffTypeRemoved

DiffTypeModified

DiffTypeEqual

«function»

GenerateDiffWithOptions

+GenerateDiffWithOptions(current, desired, logger, options)(*ResourceDiff, error)

«function»

FormatDiff

+FormatDiff(diffs, options) : string

«function»

GetLineDiff

+GetLineDiff(oldText, newText) : []diffmatchpatch.Diff

«function»

NewFormatter

+NewFormatter(compact) : DiffFormatter

«function»

MakeDiffKey

+MakeDiffKey(apiVersion, kind, name) : string

\ No newline at end of file +

«interface»

DiffRenderer

+RenderDiffs(diffs, errs) : error

«interface»

CompDiffRenderer

+RenderCompDiff(output) : error

DefaultDiffRenderer

-logger logging.Logger

-diffOpts DiffOptions

-stdout io.Writer

+RenderDiffs(diffs, errs) : error

StructuredDiffRenderer

-logger logging.Logger

-format OutputFormat

-stdout io.Writer

-stderr io.Writer

+RenderDiffs(diffs, errs) : error

DefaultCompDiffRenderer

-diffRenderer DiffRenderer

-logger logging.Logger

-diffOpts DiffOptions

+RenderCompDiff(output) : error

StructuredCompDiffRenderer

-logger logging.Logger

-format OutputFormat

-stdout io.Writer

-stderr io.Writer

+RenderCompDiff(output) : error

«enumeration»

OutputFormat

OutputFormatDiff

OutputFormatJSON

OutputFormatYAML

DiffOptions

+UseColors bool

+AddPrefix string

+DeletePrefix string

+ContextPrefix string

+ContextLines int

+ChunkSeparator string

+Compact bool

+OutputFormat OutputFormat

StructuredDiffOutput

+Diffs map

+Summary Summary

+Errors OutputError slice

CompDiffOutput

+CompositionDiff *CompositionDiff

+AffectedResources AffectedResourcesSummary

+XRImpacts []XRImpact

+Errors []OutputError

CompositionDiff

+Name string

+Diff *ResourceDiff

XRImpact

+Kind string

+Name string

+Namespace string

+Status XRStatus

+Downstream DownstreamChanges

«enumeration»

XRStatus

XRStatusChanged

XRStatusUnchanged

XRStatusErrored

AffectedResourcesSummary

+Total int

+Changed int

+Unchanged int

+Errored int

DownstreamChanges

+Added int

+Modified int

+Removed int

+Resources []DiffEntry

OutputError

+ResourceID string

+Message string

+ValidationFailures []ResourceValidationFailure

ResourceValidationFailure

+APIVersion string

+Kind string

+Name string

+Namespace string

+Status string

+Errors []FieldValidationError

FieldValidationError

+Type string

+Field string

+Message string

+Value any

ResourceDiff

+Gvk GroupVersionKind

+ResourceName string

+DiffType DiffType

+LineDiffs []diff

+Current Unstructured

+Desired Unstructured

+GetDiffKey() : string

«enumeration»

DiffType

DiffTypeAdded

DiffTypeRemoved

DiffTypeModified

DiffTypeEqual

\ No newline at end of file diff --git a/design/design-doc-cli-diff/layered-architecture.mermaid b/design/design-doc-cli-diff/layered-architecture.mermaid index 3feb12cb..814f15c5 100644 --- a/design/design-doc-cli-diff/layered-architecture.mermaid +++ b/design/design-doc-cli-diff/layered-architecture.mermaid @@ -7,27 +7,30 @@ flowchart TD subgraph "Application Layer" APP[AppContext] DPR[DiffProcessor] + CDPR[CompDiffProcessor] LOAD[Loader] end - + subgraph "Domain Layer" subgraph "Processing Components" DIFFCALC[DiffCalculator] SCHMVLD[SchemaValidator] RESMGR[ResourceManager] REQPRV[RequirementsProvider] + FNPRV[FunctionProvider] end - + subgraph "Rendering Components" DIFFRND[DiffRenderer] + CDIFFRND[CompDiffRenderer] DIFFFRM[DiffFormatter] end - + subgraph "Resource Components" RENDER[Render Function] end end - + subgraph "Client Layer" subgraph "Kubernetes Clients" APPLY[ApplyClient] @@ -35,12 +38,14 @@ flowchart TD SCHMA[SchemaClient] TCONV[TypeConverter] end - + subgraph "Crossplane Clients" COMP[CompositionClient] + CREV[CompositionRevisionClient] DEFN[DefinitionClient] ENVT[EnvironmentClient] FUNC[FunctionClient] + CRED[CredentialClient] TREE[ResourceTreeClient] end end @@ -52,43 +57,57 @@ flowchart TD %% Command Layer connections CMD --> APP CMD --> DPR + CMD --> CDPR CMD --> LOAD CMD <--> HELP - + %% Application Layer connections APP --> APPLY APP --> RESRC APP --> SCHMA APP --> TCONV APP --> COMP + APP --> CREV APP --> DEFN APP --> ENVT APP --> FUNC + APP --> CRED APP --> TREE - + + CDPR --> DPR + CDPR --> CDIFFRND + CDPR --> COMP + CDPR --> CREV + DPR --> DIFFCALC DPR --> SCHMVLD DPR --> RESMGR DPR --> REQPRV + DPR --> FNPRV DPR --> DIFFRND DPR --> RENDER DPR --> COMP DPR --> FUNC - + %% Domain Layer connections DIFFCALC --> RESMGR DIFFCALC --> TREE DIFFCALC --> APPLY - + SCHMVLD --> SCHMA SCHMVLD --> DEFN - + RESMGR --> RESRC - + RESMGR --> TREE + REQPRV --> RESRC REQPRV --> ENVT - + + FNPRV --> FUNC + FNPRV --> CRED + DIFFRND --> DIFFFRM + CDIFFRND --> DIFFRND %% Client Layer connections APPLY --> K8S @@ -97,9 +116,11 @@ flowchart TD TCONV --> K8S COMP --> RESRC + CREV --> RESRC DEFN --> RESRC ENVT --> RESRC FUNC --> RESRC + CRED --> RESRC TREE --> K8S %% Styling @@ -110,7 +131,7 @@ flowchart TD classDef externalSystems fill:#ffd3b6,stroke:#333,stroke-width:1px class CMD,HELP commandLayer - class APP,DPR,LOAD applicationLayer - class DIFFCALC,SCHMVLD,RESMGR,REQPRV,DIFFRND,DIFFFRM,RENDER domainLayer - class APPLY,RESRC,SCHMA,TCONV,COMP,DEFN,ENVT,FUNC,TREE clientLayer + class APP,DPR,CDPR,LOAD applicationLayer + class DIFFCALC,SCHMVLD,RESMGR,REQPRV,FNPRV,DIFFRND,CDIFFRND,DIFFFRM,RENDER domainLayer + class APPLY,RESRC,SCHMA,TCONV,COMP,CREV,DEFN,ENVT,FUNC,CRED,TREE clientLayer class K8S externalSystems diff --git a/design/design-doc-cli-diff/layered-architecture.svg b/design/design-doc-cli-diff/layered-architecture.svg index 4ea1a1f1..ec672996 100644 --- a/design/design-doc-cli-diff/layered-architecture.svg +++ b/design/design-doc-cli-diff/layered-architecture.svg @@ -1 +1 @@ -

External Systems

Client Layer

Domain Layer

Application Layer

Command Layer

Crossplane Clients

Kubernetes Clients

Resource Components

Rendering Components

Processing Components

Cmd

Help Function

AppContext

DiffProcessor

Loader

DiffCalculator

SchemaValidator

ResourceManager

RequirementsProvider

DiffRenderer

DiffFormatter

Render Function

ApplyClient

ResourceClient

SchemaClient

TypeConverter

CompositionClient

DefinitionClient

EnvironmentClient

FunctionClient

ResourceTreeClient

Kubernetes API

\ No newline at end of file +

External Systems

Client Layer

Domain Layer

Application Layer

Command Layer

Crossplane Clients

Kubernetes Clients

Resource Components

Rendering Components

Processing Components

Cmd

Help Function

AppContext

DiffProcessor

CompDiffProcessor

Loader

DiffCalculator

SchemaValidator

ResourceManager

RequirementsProvider

FunctionProvider

DiffRenderer

CompDiffRenderer

DiffFormatter

Render Function

ApplyClient

ResourceClient

SchemaClient

TypeConverter

CompositionClient

CompositionRevisionClient

DefinitionClient

EnvironmentClient

FunctionClient

CredentialClient

ResourceTreeClient

Kubernetes API

\ No newline at end of file diff --git a/design/design-doc-cli-diff/loader-validator-architecture.mermaid b/design/design-doc-cli-diff/loader-validator-architecture.mermaid index 51bf408a..bd359868 100644 --- a/design/design-doc-cli-diff/loader-validator-architecture.mermaid +++ b/design/design-doc-cli-diff/loader-validator-architecture.mermaid @@ -37,8 +37,9 @@ classDiagram <> +ValidateResources(ctx, xr, composed) error +EnsureComposedResourceCRDs(ctx, resources) error + +ValidateScopeConstraints(ctx, resource, ns, isClaimRoot) error } - + class DefaultSchemaValidator { -defClient DefinitionClient -schemaClient SchemaClient @@ -49,6 +50,7 @@ classDiagram +GetCRDs() []*CustomResourceDefinition +ValidateResources(ctx, xr, composed) error +EnsureComposedResourceCRDs(ctx, resources) error + +ValidateScopeConstraints(ctx, resource, ns, isClaimRoot) error } class ConvertToCRDs { diff --git a/design/design-doc-cli-diff/loader-validator-architecture.svg b/design/design-doc-cli-diff/loader-validator-architecture.svg index 13f4344c..06db7d05 100644 --- a/design/design-doc-cli-diff/loader-validator-architecture.svg +++ b/design/design-doc-cli-diff/loader-validator-architecture.svg @@ -1 +1 @@ -

«interface»

Loader

+Load()([]*Unstructured, error)

CompositeLoader

-loaders []Loader

+Load()([]*Unstructured, error)

FileLoader

-path string

+Load()([]*Unstructured, error)

StdinLoader

+Load()([]*Unstructured, error)

FolderLoader

-path string

+Load()([]*Unstructured, error)

«function»

LoadYamlStream

+LoadYamlStream(reader)([][]byte, error)

«function»

streamToUnstructured

+streamToUnstructured(stream)([]*Unstructured, error)

«interface»

SchemaValidator

+ValidateResources(ctx, xr, composed) : error

+EnsureComposedResourceCRDs(ctx, resources) : error

DefaultSchemaValidator

-defClient DefinitionClient

-schemaClient SchemaClient

-logger logging.Logger

-crds []*CustomResourceDefinition

+LoadCRDs(ctx) : error

+SetCRDs(crds)

+GetCRDs() : []*CustomResourceDefinition

+ValidateResources(ctx, xr, composed) : error

+EnsureComposedResourceCRDs(ctx, resources) : error

«function»

ConvertToCRDs

+ConvertToCRDs(xrds)([]*CustomResourceDefinition, error)

«function»

SchemaValidation

+SchemaValidation(ctx, resources, crds, skipWarn, skipSuccess, writer) : error

\ No newline at end of file +

«interface»

Loader

+Load()([]*Unstructured, error)

CompositeLoader

-loaders []Loader

+Load()([]*Unstructured, error)

FileLoader

-path string

+Load()([]*Unstructured, error)

StdinLoader

+Load()([]*Unstructured, error)

FolderLoader

-path string

+Load()([]*Unstructured, error)

«function»

LoadYamlStream

+LoadYamlStream(reader)([][]byte, error)

«function»

streamToUnstructured

+streamToUnstructured(stream)([]*Unstructured, error)

«interface»

SchemaValidator

+ValidateResources(ctx, xr, composed) : error

+EnsureComposedResourceCRDs(ctx, resources) : error

+ValidateScopeConstraints(ctx, resource, ns, isClaimRoot) : error

DefaultSchemaValidator

-defClient DefinitionClient

-schemaClient SchemaClient

-logger logging.Logger

-crds []*CustomResourceDefinition

+LoadCRDs(ctx) : error

+SetCRDs(crds)

+GetCRDs() : []*CustomResourceDefinition

+ValidateResources(ctx, xr, composed) : error

+EnsureComposedResourceCRDs(ctx, resources) : error

+ValidateScopeConstraints(ctx, resource, ns, isClaimRoot) : error

«function»

ConvertToCRDs

+ConvertToCRDs(xrds)([]*CustomResourceDefinition, error)

«function»

SchemaValidation

+SchemaValidation(ctx, resources, crds, skipWarn, skipSuccess, writer) : error

\ No newline at end of file diff --git a/design/design-doc-cli-diff/package-overview-fixed.mermaid b/design/design-doc-cli-diff/package-overview-fixed.mermaid index 505e7e4d..021ac0b3 100644 --- a/design/design-doc-cli-diff/package-overview-fixed.mermaid +++ b/design/design-doc-cli-diff/package-overview-fixed.mermaid @@ -1,106 +1,145 @@ classDiagram - class Cmd { + class XRCmd { + +Files string slice + +CommonCmdFields + +Run(ctx, log, appCtx, proc, loader, exitCode) error + } + + class CompCmd { + +Files string slice +Namespace string - +Files []string + +IncludeManual bool + +Resources string slice + +CommonCmdFields + +Run(ctx, log, appCtx, proc, loader, exitCode) error + } + + class CommonCmdFields { + +Context string + +Output OutputFormat +NoColor bool +Compact bool - +Timeout time.Duration - +Run(context, logger, appCtx, processor, loader) error + +MaxNestedDepth int + +MaxIterations int + +Timeout duration + +IgnorePaths string slice + +FunctionCredentials string slice + +FunctionRegistryOverride string + +EventualState bool } - + class AppContext { +K8sClients kubernetes.Clients +XpClients crossplane.Clients +Initialize(ctx, logger) error } - + class DiffProcessor { <> - +PerformDiff(ctx, stdout, resources) error + +PerformDiff(ctx, resources, compositionProvider) (bool, error) + +DiffSingleResource(ctx, res, compositionProvider) (map, error) +Initialize(ctx) error + +Cleanup(ctx) error } - - class DefaultDiffProcessor { - -fnClient FunctionClient - -compClient CompositionClient - -schemaValidator SchemaValidator - -diffCalculator DiffCalculator - -diffRenderer DiffRenderer - +PerformDiff(ctx, stdout, resources) error + + class CompDiffProcessor { + <> + +DiffComposition(ctx, comps, namespace, resources) (bool, error) +Initialize(ctx) error + +Cleanup(ctx) error } - + class DiffCalculator { <> - +CalculateDiff(ctx, composite, desired) error - +CalculateDiffs(ctx, xr, desired) error + +CalculateDiff(ctx, composite, desired) (Diff, error) + +CalculateDiffs(ctx, xr, desired) (map, error) + +CalculateNonRemovalDiffs(ctx, xr, parent, desired) (map, set, error) + +CalculateRemovedResourceDiffs(ctx, xr, rendered) (map, error) } - + class ResourceManager { <> - +FetchCurrentObject(ctx, composite, desired) error - +UpdateOwnerRefs(parent, child) error + +FetchCurrentObject(ctx, composite, desired) (obj, bool, error) + +UpdateOwnerRefs(ctx, parent, child) + +FetchObservedResources(ctx, xr) (resources, error) } - + class SchemaValidator { <> +ValidateResources(ctx, xr, composed) error +EnsureComposedResourceCRDs(ctx, resources) error + +ValidateScopeConstraints(ctx, resource, ns, isClaimRoot) error + } + + class FunctionProvider { + <> + +GetFunctionsForComposition(comp) (functions, error) + +Cleanup(ctx) error } - + class DiffRenderer { <> - +RenderDiffs(stdout, diffs) error + +RenderDiffs(diffs, errs) error } - + + class CompDiffRenderer { + <> + +RenderCompDiff(output) error + } + class Loader { <> - +Load() error + +Load() (resources, error) } - + class KubernetesClients { +Apply ApplyClient +Resource ResourceClient +Schema SchemaClient +Type TypeConverter } - + class CrossplaneClients { +Composition CompositionClient + +CompositionRevision CompositionRevisionClient +Definition DefinitionClient +Environment EnvironmentClient +Function FunctionClient + +Credential CredentialClient +ResourceTree ResourceTreeClient } - - class Inputs { - +CompositeResource *Unstructured - +Composition *Composition - +Functions []Function - +ExtraResources []Unstructured - } - - class Outputs { - +CompositeResource *Unstructured + + class CompositionOutputs { + +CompositeResource Unstructured +ComposedResources []Unstructured +Results []Unstructured - +Requirements map[string]Requirements + +Context Unstructured + +RequiredResources []ResourceSelector + +RequiredSchemas []SchemaSelector } - + class RenderFunc { <> - +Render(ctx, log, inputs) (outputs, error) - } - - Cmd --> AppContext - Cmd --> DiffProcessor - Cmd --> Loader - - DefaultDiffProcessor ..|> DiffProcessor - DefaultDiffProcessor --> DiffCalculator - DefaultDiffProcessor --> SchemaValidator - DefaultDiffProcessor --> ResourceManager - DefaultDiffProcessor --> DiffRenderer - + +Render(ctx, log, inputs) (CompositionOutputs, error) + } + + XRCmd --> CommonCmdFields + CompCmd --> CommonCmdFields + XRCmd --> AppContext + CompCmd --> AppContext + XRCmd --> DiffProcessor + CompCmd --> CompDiffProcessor + XRCmd --> Loader + CompCmd --> Loader + + CompDiffProcessor --> DiffProcessor : embeds + + DiffProcessor --> DiffCalculator + DiffProcessor --> SchemaValidator + DiffProcessor --> ResourceManager + DiffProcessor --> FunctionProvider + DiffProcessor --> DiffRenderer + CompDiffProcessor --> CompDiffRenderer + AppContext --> KubernetesClients AppContext --> CrossplaneClients diff --git a/design/design-doc-cli-diff/package-overview-fixed.svg b/design/design-doc-cli-diff/package-overview-fixed.svg index 4548fa2e..5cb454cd 100644 --- a/design/design-doc-cli-diff/package-overview-fixed.svg +++ b/design/design-doc-cli-diff/package-overview-fixed.svg @@ -1 +1 @@ -

Cmd

+Namespace string

+Files []string

+NoColor bool

+Compact bool

+Timeout time.Duration

+Run(context, logger, appCtx, processor, loader) : error

AppContext

+K8sClients kubernetes.Clients

+XpClients crossplane.Clients

+Initialize(ctx, logger) : error

«interface»

DiffProcessor

+PerformDiff(ctx, stdout, resources) : error

+Initialize(ctx) : error

DefaultDiffProcessor

-fnClient FunctionClient

-compClient CompositionClient

-schemaValidator SchemaValidator

-diffCalculator DiffCalculator

-diffRenderer DiffRenderer

+PerformDiff(ctx, stdout, resources) : error

+Initialize(ctx) : error

«interface»

DiffCalculator

+CalculateDiff(ctx, composite, desired) : error

+CalculateDiffs(ctx, xr, desired) : error

«interface»

ResourceManager

+FetchCurrentObject(ctx, composite, desired) : error

+UpdateOwnerRefs(parent, child) : error

«interface»

SchemaValidator

+ValidateResources(ctx, xr, composed) : error

+EnsureComposedResourceCRDs(ctx, resources) : error

«interface»

DiffRenderer

+RenderDiffs(stdout, diffs) : error

«interface»

Loader

+Load() : error

KubernetesClients

+Apply ApplyClient

+Resource ResourceClient

+Schema SchemaClient

+Type TypeConverter

CrossplaneClients

+Composition CompositionClient

+Definition DefinitionClient

+Environment EnvironmentClient

+Function FunctionClient

+ResourceTree ResourceTreeClient

Inputs

+CompositeResource *Unstructured

+Composition *Composition

+Functions []Function

+ExtraResources []Unstructured

Outputs

+CompositeResource *Unstructured

+ComposedResources []Unstructured

+Results []Unstructured

+Requirements map[string]Requirements

«function»

RenderFunc

+Render(ctx, log, inputs)(outputs, error)

\ No newline at end of file +

embeds

XRCmd

+Files string slice

+CommonCmdFields

+Run(ctx, log, appCtx, proc, loader, exitCode) : error

CompCmd

+Files string slice

+Namespace string

+IncludeManual bool

+Resources string slice

+CommonCmdFields

+Run(ctx, log, appCtx, proc, loader, exitCode) : error

CommonCmdFields

+Context string

+Output OutputFormat

+NoColor bool

+Compact bool

+MaxNestedDepth int

+MaxIterations int

+Timeout duration

+IgnorePaths string slice

+FunctionCredentials string slice

+FunctionRegistryOverride string

+EventualState bool

AppContext

+K8sClients kubernetes.Clients

+XpClients crossplane.Clients

+Initialize(ctx, logger) : error

«interface»

DiffProcessor

+PerformDiff(ctx, resources, compositionProvider)(bool, error)

+DiffSingleResource(ctx, res, compositionProvider)(map, error)

+Initialize(ctx) : error

+Cleanup(ctx) : error

«interface»

CompDiffProcessor

+DiffComposition(ctx, comps, namespace, resources)(bool, error)

+Initialize(ctx) : error

+Cleanup(ctx) : error

«interface»

DiffCalculator

+CalculateDiff(ctx, composite, desired)(Diff, error)

+CalculateDiffs(ctx, xr, desired)(map, error)

+CalculateNonRemovalDiffs(ctx, xr, parent, desired)(map, set, error)

+CalculateRemovedResourceDiffs(ctx, xr, rendered)(map, error)

«interface»

ResourceManager

+FetchCurrentObject(ctx, composite, desired)(obj, bool, error)

+UpdateOwnerRefs(ctx, parent, child)

+FetchObservedResources(ctx, xr)(resources, error)

«interface»

SchemaValidator

+ValidateResources(ctx, xr, composed) : error

+EnsureComposedResourceCRDs(ctx, resources) : error

+ValidateScopeConstraints(ctx, resource, ns, isClaimRoot) : error

«interface»

FunctionProvider

+GetFunctionsForComposition(comp)(functions, error)

+Cleanup(ctx) : error

«interface»

DiffRenderer

+RenderDiffs(diffs, errs) : error

«interface»

CompDiffRenderer

+RenderCompDiff(output) : error

«interface»

Loader

+Load()(resources, error)

KubernetesClients

+Apply ApplyClient

+Resource ResourceClient

+Schema SchemaClient

+Type TypeConverter

CrossplaneClients

+Composition CompositionClient

+CompositionRevision CompositionRevisionClient

+Definition DefinitionClient

+Environment EnvironmentClient

+Function FunctionClient

+Credential CredentialClient

+ResourceTree ResourceTreeClient

CompositionOutputs

+CompositeResource Unstructured

+ComposedResources []Unstructured

+Results []Unstructured

+Context Unstructured

+RequiredResources []ResourceSelector

+RequiredSchemas []SchemaSelector

«function»

RenderFunc

+Render(ctx, log, inputs)(CompositionOutputs, error)

\ No newline at end of file