ADR 0001 - AppCat Framework 2.0: ServiceBundle to Crossplane Converter (Slapper)
Author |
Simon Beck |
|---|---|
Owner |
Schedar |
Reviewers |
Schedar |
Date Created |
2026-06-05 |
Date Updated |
2026-06-15 |
Status |
draft |
Tags |
framework, framework-2-0, converter, crossplane |
Summary
Introduce slap, a small Go CLI that converts a single maintainer-authored ServiceBundle YAML into a Crossplane v2 CompositeResourceDefinition and a Crossplane Composition, replacing the current monorepo function approach.
Context
AppCat services today require maintainers to write the deploymet and operation logic in a single huge go Crossplane function. The Framework 2.0 effort wants a single declarative input per service, and a separate, versioned "stdlib" of Crossplane pipeline functions that each make up the Crossplane compositions.
Constraints feeding the decision:
-
Crossplane v2 introduced namespaced XRDs (no separate Claim resource). The framework targets v2.
-
Service maintainers are not Go developers; the input format must be YAML and authorable without codegen.
-
Schema authoring in raw OpenAPI v3 is verbose and error-prone; kro’s
SimpleSchemaalready solves this for the kro project and produces a valid OpenAPI v3 subschema. -
The stdlib of pipeline functions (provisioning, networking, backup, monitoring, maintenance) is shipped out-of-tree as a versioned artifact; the converter must be able to resolve and load it (see ADR 0002). Custom steps remain supported so maintainers can extract new functions for the stdlib.
-
Composition pipelines mix framework-owned steps (the stdlib) with per-service custom steps. Both must coexist in one declarative pipeline.
-
Crossplane’s XRD controller auto injects standard XR
spec/statusfields when generating the CRD, but only those it considers standard. Fields written by composition functions (for examplestatus.ready) are not auto injected and the SSA typed-patch path rejects them unless declared.
Decision
We will introduce a Go CLI slap (package: github.com/vshn/slapper) that takes a single ServiceBundle YAML and emits an XRD plus a Composition under xpkg/.
The implementation is anchored on the following decisions.
ServiceBundle as the single declarative input
We define one Go-typed YAML schema (pkg/servicebundle.ServiceBundle) with a small set of top-level stanzas: meta, claim, renderer, pipeline, credentials, plans. Maintainers author this once; the converter derives everything else.
kro SimpleSchema for service-specific parameters
We will embed the maintainer’s service-specific parameter schema as a kro SimpleSchema fragment under claim.simpleSchema. The converter expands it into an OpenAPI v3 subtree and adds it under spec.parameters.service in the generated XRD. Framework-owned fields (plan, instances, maintenance) sit alongside it under spec.parameters and are owned by the framework, not the maintainer.
Tagged unions for API schema
The Tagged Union pattern is a pattern where the right Go structs are loaded according to a so called discriminator/tag. This is useful to model an API in Go, without having a single huge Go struct.
Renderer, PipelineStep and CredentialValue are each modelled as a tagged union: a single discriminator field on the YAML side (type, kind, source), backed in Go by an interface (RendererSourceSpec, StepSpec, CredentialSpec).
Pipeline renderer registry with dummy placeholders
registerDefaults() populates the registry with a dummyRenderer for each built-in kind that emits a function-kcl step writing a placeholder ConfigMap. The customRenderer is permanent and passes the maintainer’s custom function reference through. Dummies are overridden at convert time by stdlib-supplied renderers when a stdlib source is configured (see ADR 0002).
Minimal XR status stub plus explicit status.ready
The XRD’s openAPIV3Schema.properties.status is left as { type: object, x-kubernetes-preserve-unknown-fields: true } so Crossplane can auto inject conditions, connectionDetails, etc. into the materialised CRD. We additionally declare status.ready: boolean because (a) Crossplane does not auto inject it and (b) the SSA typed-patch path rejects undeclared status fields written by composition functions.
Consequences
Positive:
-
Maintainers write one YAML per service. Kind/plural/schema drift between XRD and Composition is structurally impossible: both are derived from the same source.
-
Non-Go authors can express the service-specific schema in kro SimpleSchema rather than raw OpenAPI v3.
-
New renderer variants, step kinds and credential sources are additive: register a factory, no parser changes. Built-in kinds can be replaced one-by-one as the stdlib materialises real implementations.
-
The CLI is intentionally small (cobra + slog + yaml + kro simpleschema). The full converter compiles and runs without a Crossplane installation, so it is usable in CI and pre-commit.
-
Structured
log/slogoutput at--verbosity(default Info, Debug at-4) gives maintainers a deterministic trace of derived names, schema expansion and per-step renderer dispatch when something looks wrong.
Negative / neutral:
-
Tagged-union encoding means every new
Renderer/PipelineStep/CredentialValuevariant requires a new Go struct, factory and unit test. -
The placeholder dummy renderers emit valid-but-meaningless KCL ConfigMaps. With
--no-stdlib, a generated Composition will deploy but the framework-owned steps do nothing useful. This is acceptable as a debugging escape hatch. -
FuncRef.VersionConstraintis parsed and carried on the bundle struct; propagation into the generated package manifests is handled by the stdlib pipeline (see ADR 0002). Package management of the referenced Function CR is out of scope for the converter and will be handled by a future package-install step. -
status.readyis the only explicitly declared status field. Future composition functions that write other status fields (for examplestatus.host,status.endpoint) must either rely on the openx-kubernetes-preserve-unknown-fieldsslot (sufficient for read-only display) or extend the converter to declare them. -
The XRD ships as Crossplane v2 (Namespaced, no Claim). Services that need the classic Claim/XR split are not supported by the converter and must hand-author the XRD; this is a deliberate scope cut.