Repository navigation
feat(api): dynamic API group registration via ApplicationGroupDefinition (MVP) - #3448
Timofei Larkin (lllamnyp) wants to merge 3 commits into
Conversation
Add a cluster-scoped ApplicationGroupDefinition CRD that registers an additional API group for the cozystack aggregated apiserver to serve. The object name must equal spec.group (enforced by CEL), making registrations unique and immutable by construction; reserved cozystack.io / k8s.io namespaces and undotted (built-in-shaped) group names are rejected. ValidateApplicationGroup mirrors the CEL rules in Go for consumers that must not trust cluster state they did not write. ApplicationDefinition gains an optional spec.application.group selecting the group its kind is served in; empty keeps today's apps.cozystack.io behavior exactly (EffectiveGroup resolves the default in one place). The new CRD ships in the application-definition-crd package alongside applicationdefinitions. Assisted-By: Claude <[email protected]> Signed-off-by: Timofei Larkin <[email protected]>
Teach the aggregated apiserver to serve application kinds in any API group registered through an ApplicationGroupDefinition, instead of hardwiring apps.cozystack.io everywhere: - config.ApplicationConfig carries the resolved group; the startup loading path (extracted into buildResourceConfig, now unit-testable) lists ApplicationGroupDefinitions next to ApplicationDefinitions and resolves each definition's group. A definition referencing an unregistered group (dangling reference) is skipped with an error log and picked up again once the group is registered; kinds must stay unique across groups because OpenAPI schema refs are keyed by kind. - RegisterDynamicTypes registers kinds in their selected group and registers each non-default group-version (meta types, priority) the way install.Install does for the default group. - The apiserver installs one APIGroupInfo per group; the default group is always installed so its static APIService stays healthy. - The REST storage derives gvr/gvk (and hence the HelmRelease application.group label and every label-selector check) from the configured group. - OpenAPI post-processors stamp each kind with its group; the v3 processor runs per group-version document and only clones the kinds of that document's group (derived from its paths). ApplicationDefinitions with no group keep today's behavior bit for bit; group pickup is restart-based via the existing config-hash rollout (no hot reload yet). Assisted-By: Claude <[email protected]> Signed-off-by: Timofei Larkin <[email protected]>
Wire the controller side of ApplicationGroupDefinitions: - A new ApplicationGroupDefinitionReconciler materializes an aggregation-layer APIService (v1alpha1.<group>) per registration, cloning the service reference and cert-manager CA-injection annotation from the helm-managed v1alpha1.apps.cozystack.io reference so custom groups always target the same cozystack-api backend. The APIService is owned by its registration, so deleting the ApplicationGroupDefinition garbage-collects it. APIServices are managed as unstructured objects to avoid a kube-aggregator dependency; only reconciler-owned fields are enforced on update, so the cert-manager-injected caBundle survives. - The debounced cozystack-api restart now hashes ApplicationGroupDefinitions next to ApplicationDefinitions and watches both, so registering or deleting a group rolls the apiserver the same way definition changes always have. - The HelmRelease reconciler selects by the definition's effective group instead of a hardcoded apps.cozystack.io, matching the labels the REST storage now stamps. Assisted-By: Claude <[email protected]> Signed-off-by: Timofei Larkin <[email protected]>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Propose splitting the managed-app catalog out of cozystack/cozystack into separately hosted and tested catalog repositories, keeping only platform-contract apps in-tree. Motivated by core-repo bloat from the application tail and by full-suite e2e cost paid by every platform PR; the source-available licensing of some upstreams (Redis, MongoDB) is recorded as context pending CNCF review (cncf/foundation#1465), not as a settled driver. Builds on PackageSource delivery and the ApplicationGroupDefinition MVP (cozystack/cozystack#3448). Assisted-By: Claude <[email protected]> Signed-off-by: Timofei Larkin <[email protected]>
|
This PR has had no activity for 60 days and was marked |
What this PR does
Today every ApplicationDefinition is served in the single hardwired
apps.cozystack.iogroup: the group name is baked into the scheme registration, the REST storage, the OpenAPI post-processors, the HelmReleaseapplication.grouplabels, and a static helm-managed APIService. Third-party catalogs and platform extensions therefore cannot bring their own API group — everything lands in the platform's namespace.This PR extends the ApplicationDefinition mechanism (it does not replace it) with three capabilities:
ApplicationGroupDefinition, registers an additional API group to be served by the cozystack aggregated apiserver.ApplicationDefinitiongains an optionalspec.application.groupselecting the group its kind is served in. Empty keeps today's behavior exactly.API shape
The spec is deliberately minimal — one field. Requiring
metadata.name == spec.groupmakes registrations unique (object names are unique) and the group immutable (names are immutable) without extra webhook machinery. Group names must be dotted DNS-1123 subdomains, so single-label built-in Kubernetes groups (apps,batch) are unrepresentable, and thecozystack.io/k8s.ionamespaces are reserved via CEL, with the same rules re-checked in Go by every consumer of cluster state (ValidateApplicationGroup). All groups are served at the fixed versionv1alpha1, matching the built-in group; aversionsfield can be added compatibly later.Group selection and back-compat guarantee
spec.application.groupresolves through a single helper (EffectiveGroup/GroupOrDefault): empty meansapps.cozystack.io, which needs no registration and is always served. An ApplicationDefinition that does not set the field behaves bit-for-bit as today — same group, same HelmRelease labels, same discovery — and the new code paths collapse to the old ones (pinned byTestBuildResourceConfig_DefaultGroupUnchanged). The resolved group flows everywhere the old constant was baked in: REST storage gvr/gvk, theapps.cozystack.io/application.groupHelmRelease label and every label-selector check against it, the HelmRelease-updating reconciler in cozystack-controller (which previously hardcoded the group), and the OpenAPI GVK stamps.Restart-based pickup (not hot reload), and why
The apiserver picks up group changes by restart, not hot reload, riding the mechanism ApplicationDefinition changes already use today: cozystack-controller watches both CRDs, hashes their specs into the
cozystack.io/config-hashpod-template annotation of the cozystack-api Deployment (debounced), and the rollout re-reads everything at startup. Hot-adding an API group to a runningGenericAPIServeris not supported upstream (InstallAPIGroupafterPrepareRunis unsafe), so true hot reload would mean a serving-mux indirection layer — far beyond an MVP. Nothing in this architecture precludes it later: group resolution is already a pure function of the two CRD lists (buildResourceConfig), and the serving side consumes its output. Consequence: a new group's APIService may exist briefly before the restarted apiserver serves the group; discovery for that group reports unavailable for those few seconds, then converges.Aggregation registration (APIService)
A new
ApplicationGroupDefinitionReconcilerin cozystack-controller materializes an APIServicev1alpha1.<group>per registration, cloning the backend service reference and the cert-manager CA-injection annotation from the helm-managedv1alpha1.apps.cozystack.ioreference APIService, so custom groups always target the same cozystack-api backend regardless of installation layout. The APIService carries an ownerReference to its ApplicationGroupDefinition, so deleting the registration garbage-collects the APIService. Only reconciler-owned fields are enforced on update; the cert-manager-injectedcaBundleis preserved. APIServices are managed as unstructured objects to avoid growing a kube-aggregator dependency for one object shape.Dangling references and collisions
...apps.v1alpha1.<Kind>); the second group claiming a kind is skipped with an error log.Deferred (named, not stubbed silently)
apiGroups: ["apps.example.com"]); the tenant RBAC the platform ships only coversapps.cozystack.io. A follow-up could stamp aggregated ClusterRoles per registered group.v1alpha1fixed; aversionsfield is a compatible future addition.Verification
go build ./...andgo vet ./...clean;make generatere-run to convergence (zero drift — deepcopy, CRD YAML, and openapi are all tool-generated).pkg/cmd/server/groups_test.go); multi-group scheme registration (pkg/apis/apps/v1alpha1/register_test.go); per-group OpenAPI v3 document filtering and v2 GVK stamping (pkg/cmd/server/openapi_groups_test.go); APIService create/update-preserving-caBundle/invalid-group paths and the config-hash extension (internal/controller/applicationgroupdefinition_controller_test.go).Downstream repositories
Boxes are deliberately left empty rather than ticked: this is a draft MVP and filing downstream PRs now would be premature. Walking the trigger map against the diff, if this merges as-is two repositories are reached: cozystack/website (
content/en/docs/next/cozystack-api/application-definitions.mddocuments ApplicationDefinition semantics, which gain thegroupfield and the new companion CRD) and cozystack/external-apps-example (the ApplicationDefinition CRD inpackages/system/application-definition-crd/definition/changed — additively only, an optional field and a new CRD file, so nothing there breaks, but the example is the natural place to demonstrate a third-party group). Follow-ups should be filed when this graduates from proof-of-concept.Release note