Complete field reference for the App custom resource
(tendtech.io/v1alpha1) — the single source of truth for one Tend-governed
application. Read this when you need the exact type, validation, default, or
writer of any spec or status field. For the reasoning behind the model see
the governance model and
app lifecycle; for creating one see
onboard a new app.
Source of truth: operator/api/v1alpha1/app_types.go.
STATE (.status.state), OWNER (.spec.owner), EXPIRY (.spec.expiry), AIACT (.spec.purpose.decisionsAboutPeople)
spec is user-written (the owner and platform tooling); status is written
only by the Tend operator. The build pipeline and promotion wiring report
into the CR via annotations (below), never by writing status directly.
MinLength=1. The Keycloak identity responsible for this App. No App exists unowned; deactivation past the grace period freezes the app (see app lifecycle).
expiry
timestamp (metav1.Time)
yes
now + platform TTL, at admission
When the App freezes if not renewed. Renew via the tendtech.io/renew annotation or by editing this field directly — see app lifecycle.
Declared, named, typed, access-scoped sources reachable through the data gateway. List type map keyed on name — names are unique, schema-enforced.
resourceCaps
ResourceCaps
no (defaulted)
platform policy ("1" / "1Gi")
Per-app compute ceilings. The operator renders them into a per-namespace ResourceQuota/LimitRange; Kyverno validates workloads against them.
costCap
CostCap
no (defaulted)
platform policy ("50.00" EUR monthly)
Per-app spend ceiling driving cost warnings and suspension.
purpose
PurposeClassification
no — required at promotion
never defaulted
What the App is for. The promotion path refuses submission without it (422 gate).
repoRef
RepoRef
no
—
The per-app Gitea repository and its tracks. Populated by the platform at onboarding (001 contract: operator-populated), read by the promotion path.
builder
string
no
—
The integrated builder/adapter that produced the App.
tier
enum tier1 | tier2
no
tier1 (CRD default)
The inference boundary the App's builder runs against. Tier 2 is reserved and not exercised this slice. See boundary tiers.
No network, ingress, or egress declaration fields exist on the spec. The
network perimeter is derived from dataScopes by the operator, never
declared; a manifest carrying such fields fails schema validation as unknown
fields under structural pruning. The rendering decision is
ADR-0007.
The Kyverno mutate ClusterPolicy app-field-defaults
(deploy/policies/kyverno/app-field-defaults.yaml) fills missing governance
fields at App CREATE. Mutating webhooks run before validating webhooks,
so a bare App is defaulted and then passes the app-required-fields
validation backstop in the same admission. Defaults are written onto the CR
(explicit over implicit): what admission defaulted is what the estate shows
and what promotion snapshots. Values come from the GitOps-delivered
ConfigMap policy-defaults in namespace kyverno.
never defaulted — optional until promotion, strict at the promotion 422 gate
—
Each rule uses Kyverno's +(field) add-if-absent anchor: an explicitly set
field is never overwritten. Changing a default is a ConfigMap edit that Argo
CD reconciles — no policy change. The admission engine decision is
ADR-0006.
Lifecycle state: sandbox | promotion-pending | production | frozen | retired. frozen/retired are live (005-lifecycle-estate) — see app lifecycle for causes, retention, and restore.
lifecycle
LifecycleStatus
Operator-owned lifecycle posture: ownerState, orgUnit, freeze (causes + since), priorState, lastRenewedAt/retiredAt/restoredAt, archiveRef, revokedScopes, and the durable history of every lifecycle transition. See app lifecycle and estate portal & revoked scopes.
namespace
string
The provisioned per-app workload namespace (tend-app-<name>).
sandboxArtifact
ArtifactRef
Latest attested sandbox image + evidence.
productionArtifact
ArtifactRef
Set on a successful promotion.
attestationRefs
[]ArtifactRef
History of attested artifacts, one per iteration.
promotion
PromotionStatus
The current/last promotion. The PromotionRequest entity lives inline here — there is no separate promotion store.
normal | warning-75 | warning-90 | suspended | unknown. unknown means cost telemetry is missing and is never enforced on — an app is not punished for a telemetry gap.
lastEvaluated
timestamp
Last successful cost poll.
lastTransition
timestamp
When state last changed.
warning75At
timestamp
Set once per period-crossing of the 75% threshold (notify once per crossing, oscillation-safe); cleared at period rollover.
warning90At
timestamp
The 90% counterpart of warning75At.
lastSuspendedAt
timestamp
Durable trace of the last cost suspension. Persists after resume; never cleared at period rollover — an audit trail, not a per-period guard.
lastResumedAt
timestamp
Set when a suspension is lifted (cap raise or resume annotation).
Cost enforcement is deliberately not a status.state value: a
production app suspended for cost is still state: production with the
Suspended condition true and costStatus.state: suspended. See
respond to a cost-cap suspension.
Why enforcement fired; CostCapBreached (the cost ladder) or ManualSuspend (a platform engineer's estate suspend action). The two are independent — the cost reconciler never clears a manual suspension.
at
timestamp
When enforcement fired.
resumableBy
string
Documents the resume contract: owner raises spec.costCap.amount above accrued cost, or applies the tendtech.io/resume annotation.
spec.dataScopes, spec.resourceCaps, spec.costCap are
owner-mutable at any time. Scope changes reconcile into enforced
behavior without redeploy (target ≤5 minutes, SC-004).
Raising spec.costCap.amount above accrued cost is one of the two resume
paths. Lowering it below accrued triggers breach evaluation at the next
cost reconcile.
The tendtech.io/resume: "true" annotation is the explicit resume path. The
operator consumes (removes) it, so a stale annotation cannot pin an
app un-suspendable across future breaches. The resume is recorded in
evidence.
status.* is operator-only. Users, the builder, and the pipeline never
write status; they report via the annotation contracts below.
Operator-owned labels, annotations, and finalizer¶
tend-operator on every per-app object the operator generates (Cilium policies, gateway discovery ConfigMap, ResourceQuota/LimitRange). The provenance check asserts every per-app policy object carries it — never hand-written.
tendtech.io/policy
label
Policy-specific marker on generated Cilium policies.
Guards workload-namespace cleanup: deleting an App first releases its per-app namespace (and everything that cascades with it) before the App record is removed.
001 stored dataScopes as a []string enum whose only value was
"synthetic". Governance depth (003) made DataScope a struct. A one-time,
idempotent migration (hack/migrate-scopes.sh, unstructured read →
rewrite; no conversion webhook) maps legacy "synthetic" →
{name: synthetic, sourceType: synthetic, access: readwrite} in both
spec.dataScopes and status.promotion.declared.dataScopes, and must run
before the struct-form CRD schema is applied.