External project integration¶
This page defines the boundary for a consuming platform that wants a project custom resource (CR) to result in a Grafana Cloud stack request. The project CR shape below is an illustrative adapter contract. This repository does not install that CRD, watch its cluster, or run the adapter.
The request contract is the GrafanaCloudStackRequest XRD in platform/apis/stack-v1beta1.yaml. It requires spec.displayName, spec.slug, spec.region, spec.usage, and spec.organization. The XRD also requires metadata.name == spec.slug, and keeps slug, usage, organization, and profile immutable after creation.
Trigger boundary¶
The external-CR trigger belongs to the consuming platform. The intended flow is:
- The consuming platform observes its own project CR.
- Its adapter normalizes the project into a candidate
GrafanaCloudStackRequestand a Kustomize directory. - The adapter opens a pull request adding that directory below
enabled/. - A person reviews and merges the pull request.
- The repository's Argo CD ApplicationSet applies the merged directory. Its only input is the top-level
enabled/*Git generator path, as shown indeploy/argocd/requests-applicationset.yaml.
There is no external-project controller, generator, webhook, or second watch path in this repository. The catalog example in examples/catalog/external-project/ is outside enabled/ and is inert.
Alternatives considered¶
The consuming-platform boundary is deliberate:
- A controller in this repository would encode one consumer's internal CRD shape in a public reference. It would also add a runtime image, publish and signature workflow, digest pin, RBAC, and a second serialized publishing bottleneck beside the composition function.
- An Argo Plugin generator is an HTTP service, so it is a runtime component under another name. Its claim would not land in Git, so it would not provide a reviewable generated artifact before reconciliation.
- A consuming-platform adapter plus a pull request into
enabled/keeps the trigger with the owner of the project CR, gives the generated claim a Git review boundary, and preserves this repository as a portable public reference.
The absence of a trigger component here is the public-reference decision, not an unfinished implementation. The consuming platform owns the adapter and must adapt its real project CR fields to the normalized contract below.
Normalized project shape¶
The adapter consumes this minimal shape. metadata.name is constrained by the adapter to lowercase alphanumeric characters, three to sixteen characters. A consumer with a different source CR uses an explicit field adapter to produce the same normalized values; it must not guess a field or silently fall back to a default.
apiVersion: tenancy.example.org/v1alpha1
kind: Project
metadata:
name: demo
namespace: platform-projects
spec:
displayName: Example development project
environment: development
organization: example-primary
spec.environment is the external name for the request's usage class. This contract permits only the constrained enum development or production. That is narrower than the XRD's syntactic lowercase-and-hyphen pattern. The platform-owned Composition input remains the semantic authority: the selected organization entry must allow the usage and the selected region, and the global allowedUsages list must allow the usage too. A value such as preview, even if it matches the XRD's syntax, is refused by this adapter.
spec.organization is required. The adapter copies the exact value into the request and treats it as immutable. It must resolve to a platform registry entry with a provider configuration, an allowed region, and an allowed usage. There is no organization fallback. A missing, malformed, unknown, or changed organization stops mapping and produces no request.
Field mapping¶
The table distinguishes project-CR values from values fixed by the consuming platform's request template. The example values in the final column are public shapes, not source-environment identity.
| Project CR field | Request field | Ownership and rule | Example |
|---|---|---|---|
metadata.name | metadata.name and spec.slug | CR-sourced identity segment. Build the slug below; never truncate it. The two request fields must be identical. | demo |
spec.displayName | spec.displayName | CR-sourced, required, and validated against the XRD's 1 to 128 character string limit. | Example development project |
spec.environment | spec.usage | CR-sourced through the closed enum development or production; immutable in the request. | development |
spec.organization | spec.organization | CR-sourced, required, registry-validated, and immutable in the request. | example-primary |
| No project field | metadata.namespace | Platform-fixed target namespace selected by the consuming platform's Argo destination. | grafana-vending |
| No project field | spec.region | Platform-fixed by the organization and usage profile. It must match the XRD region pattern and the registry's allowedRegions. | prod-us-central-0 |
| No project field | spec.profile | Platform-fixed configuration profile. The XRD makes it immutable. | standard |
| No project field | spec.lifecycle.externalResources | Fixed to Retain for generated requests. The project CR cannot arm external deletion. | Retain |
| No project field | baseline and optional feature fields | Platform-fixed template defaults: baseline dashboards and telemetry access enabled; plugins empty; dashboards and home preference createOnly; SSO, reports, and incident integration disabled; product toggles false. | See the worked request |
The template's optional fields are still real request fields. If a consumer chooses to emit them explicitly, the fixed values are:
spec.products.applicationObservability,spec.products.kubernetesObservability, andspec.products.databaseObservability:false;spec.baselineDashboards.enabledandspec.telemetryAccess.enabled:true;spec.plugins:[];spec.reconciliation.dashboardsandspec.reconciliation.homePreference:createOnly;spec.sso.mode:disabled, with no profile;spec.monthlyReport.enabled:false, with no recipients or reply-to;spec.incidentIntegration.enabled:false, with no profile andtemplateMode: createOnly.
spec.retention and spec.expiry are not project inputs. They are creation-time platform decisions and are omitted from this minimal contract; if a consuming platform adds either, it must select the value from a reviewed platform policy rather than the source CR.
The output identity is deterministic and valid for the actual XRD slug pattern, which permits lowercase alphanumeric characters only:
normalizedOrganization = remove '-' from spec.organization
slug = normalizedOrganization + spec.environment + metadata.name
metadata.name = slug
For the worked shape, example-primary, development, and demo produce exampleprimarydevelopmentdemo. The adapter must refuse the candidate when the result is shorter than three characters, longer than 32 characters, not ^[a-z0-9]+$, or already identifies another request in the target namespace. It must also refuse a normalization collision between two registered organizations. Truncating the slug, choosing a different separator, or silently reusing an existing request would break the API's identity contract.
The adapter does not copy provider IDs, stack URLs, ProviderConfig names, credential paths, secret references, tokens, or vendor endpoints from a project CR. Those values are either provider-observed or platform-owned and are outside this trigger contract.
Refusal before a pull request¶
Mapping is fail-closed. The adapter must reject the candidate and leave no request under enabled/ when any of these checks fails:
- a required source value is missing, the display name is outside the XRD length, or the source identity cannot produce a valid slug;
environmentis notdevelopmentorproduction;organizationis absent, malformed, unknown, or not immutable for the existing request;- the platform-fixed region is outside the XRD pattern or is not allowed by the selected organization entry;
- the organization entry does not allow the selected usage, or the global usage allow-list does not contain it;
- the generated identity collides, exceeds the XRD's 32-character limit, or would change an existing
metadata.nameor immutable request field; - the source CR tries to override a platform-fixed field such as region, profile, lifecycle, provider identity, or credential output.
The refusal is a mapping or schema failure, not a vendor refusal. A rejected candidate must not be retried against Grafana Cloud and must not be represented as an applied stack request.
Review, cap, and failure classification¶
The pull request is the approval seam and the explicit cap. The consuming platform must not auto-merge generated requests. The default bounded unit is one candidate project per pull request, so every new stack entering reconciliation consumes one human approval. The review checks the normalized field mapping, identity uniqueness, organization and region policy, the fixed Retain lifecycle, and the requested number of pending projects.
This seam bounds how many generated requests can enter the watched Git tree. It does not bound the number of project CRs that can be created, vendor quotas or spend, or the number of requests that were already approved and merged. Those are consuming-platform policy concerns. A queue, budget, or stricter batch limit may be added by that platform, but it cannot bypass the review seam in this contract.
The review artifact and the Kubernetes condition chain make templating errors distinct from vendor refusals without provider logs:
| Failure class | Where it appears | Meaning and action |
|---|---|---|
| Mapping error | Adapter or pull-request check, before merge | The normalized source values cannot produce a valid request. Do not merge or apply anything. |
| Request schema error | API-server admission or Argo apply status, before provider reconciliation | The generated YAML violates the XRD, for example a missing required field or metadata.name != spec.slug. Fix the template and rerun the mapping check. |
| Vendor refusal | An applied request's composed child status.conditions and Kubernetes events | The request passed mapping and API admission, but a provider-managed child reports its own Synced=False or Ready=False condition. Investigate that child and its condition message; provider logs are not required for classification. |
| Credential health signal | The applied composite's conditions, with the expected child condition | The GCV-0087 shape makes a configured rotating-token family with a missing child or a child without Synced=True force the owning composite to Ready=False. It is an observed health signal, not a mapping error and not a custom provider field. |
The standard condition shape is intentionally small:
# Applied GrafanaCloudStackRequest, after a valid mapping
status:
conditions:
- type: Ready
status: "False"
# One observed rotating-token child, when it is the failing witness
status:
conditions:
- type: Synced
status: "False"
reason: CannotUpdateExternalResource
The CannotUpdateExternalResource reason is an example of a provider condition reason when emitted, not a reason the adapter invents. Ordinary provider failures can leave a composite's own readiness less specific, so the child condition and event remain part of the diagnosis. GCV-0087 specifically closes the rotating-token visibility gap by making missing or unsynced expected children visible through composite Ready=False; it does not claim that a generic Ready=False is proof of a template error.
Decommission and project disappearance¶
Project-CR disappearance is not a deletion signal. The adapter does not remove the generated Git request, change its lifecycle, or set Delete when the source CR disappears. A generated request remains in Git until a separate reviewed change removes or changes it.
The request XRD defaults spec.lifecycle.externalResources to Retain. Removing an unarmed request from Git therefore orphans external resources. The only destructive route is the existing owner-authorised decommission path in docs/governance.md:
- A platform owner adds the exact request namespace, name, Kubernetes UID, and immutable profile to platform-owned
deletionAuthorizations. - A first reviewed change sets
spec.lifecycle.externalResources: Delete. Wait forstatus.deletionArmed=trueand thenstatus.deletionReady=true. Readiness requires observeddeleteProtection=false, prepared administrator and telemetry rotating tokens, and current successful external-secret sync and finalizer state for every enabled credential document. - A second reviewed change removes dependent
GrafanaCustomRoleBinding,GrafanaTeamAccess, andGrafanaContentAccessPolicyclaims and waits for their Kubernetes objects and finalizers to disappear while the request and Stack still exist. - A third reviewed change removes the request from Git and verifies the resulting external deletion. Stack-local content remains retain/orphan by design.
The source project CR cannot perform any of these stages. If its disappearance should eventually lead to decommission, that is a new, separately reviewed consuming-platform workflow that must still enter the owner-authorised path.
Scope and evidence¶
This repository ships the public request schema, the normalized mapping contract, the review and cap boundary, lifecycle semantics, and the inert worked example. It ships no external-project controller, generator runtime, RBAC, credentials, live request, or widened ApplicationSet watch. A rendered catalog example proves YAML and Kustomize shape only; it does not prove a live cluster, provider, vendor, or deployment transaction.