Skip to main content
Version: Next

XTenantApp

Tenant application scaffold โ€” provisions a Deployment + Service + optional IngressRoute + optional ServiceAccount for an application workload, following app.kubernetes.io/* label conventions.

tenant-app deliberately stops at the application workload. Vault-backed secrets and databases (XTenantDatabase) are provisioned separately and referenced via secretsFrom.

Groupplatform.wxops.cloud
KindXTenantApp
Pluralxtenantapps
ScopeCluster
API versionv1alpha1 (served, storage)
Composition functionfunction-kcl

What it composesโ€‹

Composed resourceCondition
DeploymentAlways
Serviceservice.enabled: true (default)
IngressRoute (Traefik)ingress.enabled: true
TraefikService (weighted split)darlane.trafficWeight > 0
Certificate (cert-manager)ingress.tls.clusterIssuer set
ServiceAccountserviceAccount.create: true
PersistentVolumeClaimvolumes[].create: true
ServiceMonitor or PodMonitormonitoring.enabled: true
Darlane Deployment (<app>-dev)darlane.enabled: true

spec.parametersโ€‹

Core identityโ€‹

FieldTypeRequiredDefaultDescription
appNamestringyesApplication name. Used as Deployment/Service/IngressRoute name and app.kubernetes.io/name label.
namespacestringyesTarget namespace for all composed resources.
clusterstring"default"provider-kubernetes ProviderConfig to compose resources through. default (the hub cluster) is the only one wired up today โ€” leaving this unset is behavior-neutral. Multi-cluster targeting is not yet functional; no spoke ProviderConfig exists yet.
environmentstring"dev"One of dev, staging, prod. Applied as wxops.cloud/environment label โ€” metadata for dashboards and Kyverno policies; does not affect composed resource behavior.
appFlavorstring"webapp"One of webapp, ai, ai-webapp, geo-webapp, search-webapp. Applied as wxops.cloud/app-flavor label for platform automation (e.g. selecting pgvector/postgis extensions). Does not affect this XR's composed resources.
templateIdstringScaffold template identifier. Applied as wxops.cloud/template-id annotation for catalog linking.
repository.urlstringSource repository URL. Applied as wxops.cloud/repo-url annotation.

Containerโ€‹

FieldTypeRequiredDefaultDescription
imagestringyesContainer image reference (repository:tag). Use the single-string form so ArgoCD Image Updater can target it via Kustomize image-transformer.
imagePullPolicystring"IfNotPresent"One of Always, IfNotPresent, Never.
imagePullSecretsarray<string>[]Names of existing kubernetes.io/dockerconfigjson Secrets in the target namespace.
replicasinteger1
containerPortinteger8080
terminationGracePeriodSecondsinteger30Seconds between SIGTERM and SIGKILL. Increase for apps with long-running requests or connection draining.
resourcesobjectrequests/limits ร— cpu/memory, passed through verbatim.
envarray<{name, value}>[]Plain (non-secret) environment variables.
envFromarray<{secretRef|configMapRef: {name}}>[]Additional envFrom sources, merged after secretsFrom-managed refs.
podAnnotationsobject{}Annotations on the pod template. Not how Prometheus scraping is configured โ€” use monitoring below; this platform's Prometheus doesn't watch prometheus.io/* annotations.
deploymentAnnotationsobject{}Extra annotations on the Deployment metadata.
labelsobject{}Extra labels on all composed resources. Standard app.kubernetes.io/* and wxops.cloud/* labels always win โ€” selectors depend on them.
commandarray<string>image defaultOverride container ENTRYPOINT.
argsarray<string>image defaultOverride container CMD.

rolloutStrategyโ€‹

FieldTypeDefaultDescription
rolloutStrategy.typestring"RollingUpdate"One of RollingUpdate, Recreate.
rolloutStrategy.rollingUpdate.maxSurgestring|integer"25%"Only when type: RollingUpdate.
rolloutStrategy.rollingUpdate.maxUnavailablestring|integer"25%"Only when type: RollingUpdate.

serviceAccountโ€‹

FieldTypeDefaultDescription
serviceAccount.createbooleanfalseIf true, compose a new ServiceAccount in the target namespace.
serviceAccount.namestringappNameSA name. When create: false, must reference an existing SA.
serviceAccount.annotationsobject{}Annotations on the created SA. Use for workload identity bindings (eks.amazonaws.com/role-arn, iam.gke.io/gcp-service-account). Ignored when create: false.

securityContextโ€‹

Pod-level fields apply to all containers; container-level fields apply to the main app container (and darlane when enabled).

FieldTypeDefaultDescription
securityContext.runAsNonRootbooleanPod-level. Require all containers to run as non-root.
securityContext.runAsUserintegerPod-level. UID for all containers.
securityContext.runAsGroupintegerPod-level. Primary GID for all containers.
securityContext.fsGroupintegerPod-level. GID applied to mounted volumes.
securityContext.readOnlyRootFilesystembooleanContainer-level. Mount root filesystem read-only.
securityContext.allowPrivilegeEscalationbooleanContainer-level. Set false for hardened workloads.
securityContext.capabilities.droparray<string>[]Container-level. Capabilities to drop. Use ["ALL"] for the recommended baseline.
securityContext.capabilities.addarray<string>[]Container-level. Capabilities to add back (e.g. ["NET_BIND_SERVICE"]).

secretsFromโ€‹

Wires envFrom.secretRef entries by name. This XR only adds the reference โ€” it does not provision or wait for the Secret. The Deployment will fail to start (CreateContainerConfigError) until referenced Secrets exist.

FieldTypeDefaultDescription
secretsFrom.app.enabledbooleanfalseAdds envFrom.secretRef โ†’ secretsFrom.app.secretName.
secretsFrom.app.secretNamestring"{appName}-env"Name of the app env Secret.
secretsFrom.database.enabledbooleanfalseAdds envFrom.secretRef โ†’ secretsFrom.database.secretName.
secretsFrom.database.secretNamestring"{appName}-db-creds"Name of the database credentials Secret, written by XTenantDatabase.

reloaderโ€‹

FieldTypeDefaultDescription
reloader.enabledbooleanfalseAdds reloader.stakater.com/auto: "true" to the Deployment. Triggers a rolling restart when referenced ConfigMaps/Secrets change. Requires Stakater Reloader in-cluster.

volumesโ€‹

Mount volumes into the main app container. Supply exactly one discriminator per entry.

DiscriminatorSource
claimNamePersistentVolumeClaim โ€” reference existing, or create with create: true
configMapNameNamed ConfigMap
secretNameNamed Secret
FieldTypeDefaultDescription
volumes[].namestringrequiredVolume name.
volumes[].mountPathstringrequiredAbsolute mount path inside the container.
volumes[].subPathstringSubpath within the volume.
volumes[].readOnlybooleanfalse
volumes[].claimNamestringPVC name. Mutually exclusive with configMapName/secretName.
volumes[].createbooleanfalseIf true, compose a new PVC named claimName.
volumes[].storageClassstringStorageClass for the new PVC. Required when create: true.
volumes[].sizestringPVC capacity (e.g. "10Gi"). Required when create: true.
volumes[].accessModesarray<string>["ReadWriteOnce"]Only used when create: true.
volumes[].configMapNamestringName of an existing ConfigMap. Mutually exclusive with claimName/secretName.
volumes[].secretNamestringName of an existing Secret. Mutually exclusive with claimName/configMapName.
volumes[].items[]arrayKey-to-path projections. Each entry: {key, path}.

serviceโ€‹

FieldTypeDefaultDescription
service.enabledbooleantrue
service.typestring"ClusterIP"One of ClusterIP, NodePort, LoadBalancer.
service.portinteger80Service port. targetPort is always containerPort.

probesโ€‹

HTTP GET probes against containerPort. liveness and readiness default to enabled when service.enabled is true โ€” implementing GET /healthz and GET /readyz on containerPort is a golden-path contract for all scaffolded services.

FieldTypeDefaultDescription
probes.liveness.enabledbooleanservice.enabled
probes.liveness.pathstring"/healthz"
probes.liveness.initialDelaySecondsinteger10
probes.liveness.periodSecondsinteger10
probes.liveness.timeoutSecondsinteger1
probes.liveness.failureThresholdinteger3
probes.readiness.enabledbooleanservice.enabled
probes.readiness.pathstring"/readyz"
probes.readiness.initialDelaySecondsinteger5
probes.readiness.periodSecondsinteger10
probes.readiness.timeoutSecondsinteger1
probes.readiness.failureThresholdinteger3
probes.startup.enabledbooleanfalseGates liveness/readiness until the first successful check. Useful for slow-starting apps.
probes.startup.pathstring"/healthz"
probes.startup.periodSecondsinteger10
probes.startup.failureThresholdinteger30

ingressโ€‹

ingress.enabled: true always emits a Traefik IngressRoute โ€” never a standard Ingress. This is required for darlane.trafficWeight to switch between a plain Service and a TraefikService weighted split with zero downtime.

FieldTypeDefaultDescription
ingress.enabledbooleanfalseEmits a Traefik IngressRoute.
ingress.hoststringRequired when enabled: true. Used as the hostname in the Traefik Host(...) match rule on the IngressRoute.
ingress.pathstring"/"URL path prefix used in the Traefik PathPrefix(...) match rule.
ingress.annotationsobject{}Merged onto IngressRoute metadata.
ingress.tls.enabledbooleanfalseSets entryPoints: [websecure] and spec.tls.secretName.
ingress.tls.secretNamestring"{appName}-tls"TLS Secret name. When clusterIssuer is set, cert-manager writes into this name.
ingress.tls.clusterIssuerstringIf set (and tls.enabled), emits a cert-manager Certificate CR. cert-manager auto-provisions the Secret. Requires cert-manager and the named ClusterIssuer in-cluster.
ingress.auth.enabledbooleanfalseAdds auth-errors and forward-auth-redirect Traefik Middleware references โ€” SSO via oauth2-proxy ForwardAuth. Both Middleware CRDs must exist in kube-system.

monitoringโ€‹

Emits a real Prometheus ServiceMonitor or PodMonitor โ€” the scaffold wizard's Monitoring toggle writes only enabled and, optionally, path; every other field defaults sensibly and is exposed here for direct XR authors.

FieldTypeDefaultDescription
monitoring.enabledbooleanfalseMaster toggle. When false, no monitor is emitted.
monitoring.kindstring"auto"auto (ServiceMonitor when service.enabled, else PodMonitor), or an explicit ServiceMonitor/PodMonitor override. Setting ServiceMonitor without a Service is a hard render-time failure, not a silent no-op.
monitoring.portintegercontainerPortPort to scrape.
monitoring.pathstring"/metrics"HTTP path exposing the metrics.
monitoring.intervalstring"30s"Scrape interval.
monitoring.scrapeTimeoutstring"10s"Per-scrape timeout. Must be <= interval.
monitoring.sampleLimitinteger5000Max samples accepted per scrape.
monitoring.honorLabelsbooleanfalseWhen true, labels from the scraped metrics win over Prometheus's own target labels on conflict.
monitoring.metricRelabelingsarray[]Prometheus metric_relabel_configs, applied at ingest.
Per-cluster provider RBAC required

Creating a ServiceMonitor/PodMonitor needs monitoring.coreos.com permissions on Crossplane's provider-kubernetes ServiceAccount โ€” a manual, per-spoke-cluster kubectl apply of providers/rbac-provider-kubernetes.yaml, not carried by any package bump. Until it's applied, monitoring.enabled: true fails forbidden at reconcile โ€” visible in this XR's own status/events. See Runtime Observability for the full picture.

darlane โ€” debug twin Deploymentโ€‹

When enabled, a second <appName>-dev Deployment is created alongside the main one โ€” same image/env/secrets, scaled to 0 by default, with no Service/IngressRoute of its own. Scale it on-demand, route real traffic via trafficWeight for A/B testing, or use mirrord CLI directly against the pod for local development with real cluster env and secrets.

Full guide

Darlane Overview โ€” exec, file sync, traffic mirroring, A/B testing, and SRE agent workflow.

Coreโ€‹

FieldTypeDefaultDescription
darlane.enabledbooleanfalse
darlane.replicasinteger0Scale to 1 on-demand to start the debug pod.
darlane.imagestringmain imageOptional image override.
darlane.commandarray<string>["sleep", "infinity"]Keeps the pod alive for kubectl exec/port-forward.
darlane.argsarray<string>Args override.
darlane.containerPortintegermain containerPortOverride when dev server starts on a different port (e.g. hot-reload on 3000 vs production on 8080).
darlane.envarray<{name,value}>[]Extra env vars layered on top of main app env. Darlane values win on key collision.
darlane.resourcesobjectIndependent resource requests/limits. Omit to inherit main app resources.
darlane.securityContextobjectApplied on top of main securityContext. readOnlyRootFilesystem defaults to false so apt/pip installs work.
darlane.productionOverridebooleanfalseRequired when environment: prod to enable darlane โ€” double opt-in, visible in XR diffs.
darlane.ttlstringAdds wxops.cloud/darlane-ttl annotation (e.g. "4h"). Enforcement via separate Kyverno policy.

Traffic routingโ€‹

Route a slice of real IngressRoute traffic to the darlane pod. Three controls compose freely.

FieldTypeDefaultDescription
darlane.trafficWeightinteger00 = debug only ยท 1โ€“99 = A/B split ยท 100 = full canary. Emits a ClusterIP Service + Traefik TraefikService weighted split. Requires ingress.enabled: true.
darlane.stickySession.enabledbooleanfalsePin browser sessions to the same backend via a Traefik cookie. Requires trafficWeight > 0.
darlane.stickySession.cookieNamestring"darlane-ab"Override if multiple apps share the same domain.
darlane.stickySession.securebooleantrueSet the Secure flag (HTTPS only).
darlane.stickySession.sameSitestring"lax"lax ยท strict ยท none (requires secure: true).
darlane.headerRouting.enabledbooleanfalsePin requests with a specific header directly to darlane โ€” bypasses trafficWeight entirely. Requires ingress.enabled: true.
darlane.headerRouting.headerstringHTTP header name (e.g. X-Target-Env). Required when enabled: true.
darlane.headerRouting.valuestringHeader value to match (e.g. darlane). Required when enabled: true.
darlane.telemetryPortintegerEmits {appName}-darlane-telemetry Service for OTEL/Prometheus. Common: 4317 (gRPC), 4318 (HTTP), 9090.

darlane.volumesโ€‹

Same discriminator pattern as main volumes[] โ€” set exactly one of claimName, configMapName, or secretName. PVC creation (create: true) is not supported in darlane volumes; reference existing claims only.

FieldTypeDefaultDescription
darlane.volumes[].namestringrequiredVolume name.
darlane.volumes[].mountPathstringrequiredAbsolute mount path.
darlane.volumes[].subPathstringSubpath within the volume.
darlane.volumes[].readOnlybooleanfalse
darlane.volumes[].claimNamestringName of an existing PVC.
darlane.volumes[].configMapNamestringName of an existing ConfigMap.
darlane.volumes[].secretNamestringName of an existing Secret.
darlane.volumes[].items[]arrayKey-to-path projections: {key, path}.

darlane.serviceAccountโ€‹

FieldTypeDefaultDescription
darlane.serviceAccount.createbooleanfalseEmits a dedicated ServiceAccount named {appName}-darlane (or name).
darlane.serviceAccount.namestring{appName}-darlaneWhen create: false, must reference an existing SA.
darlane.serviceAccount.annotationsobject{}Workload identity bindings (eks.amazonaws.com/role-arn, iam.gke.io/gcp-service-account). Ignored when create: false.

statusโ€‹

Written back by the composition โ€” read these fields directly rather than polling Crossplane's own composite Ready condition, which can lag due to WatchCircuitOpen throttling. All are derived from observed composed-resource state on each reconcile.

FieldTypeDescription
status.createdbooleanTrue once the Deployment, Service, and Ingress have been observed in the cluster at least once. Distinguishes "provisioning in progress" (created: false) from "exists but not yet healthy" (created: true, ready: false).
status.readybooleanTrue when the core workload (Deployment, Service, Ingress) reports Ready. Scoped to the core workload on purpose โ€” check dependenciesReady too before calling the app fully healthy.
status.dependenciesReadybooleanTrue when resources that gate the app being usable end-to-end, but don't gate the workload itself, report Ready โ€” today, the cert-manager Certificate when ingress.tls is enabled. Split from ready so a consumer can render "app is up, TLS still issuing" instead of a blanket "not ready." Does not cover database provisioning โ€” secretsFrom.database only wires in an existing Secret by name and is never waited on.
status.namespacestringKubernetes namespace where the app is deployed.
status.imagestringContainer image currently configured for the deployment.
status.urlstringApplication URL derived from ingress.host (https:// when tls.enabled, http:// otherwise). Empty when ingress is disabled.
status.darlaneobjectObserved state of the Darlane debug twin. Absent entirely when darlane.enabled: false โ€” not present-and-empty.
status.darlane.readybooleanTrue when the <appName>-darlane Deployment reports Ready.
status.darlane.replicasintegerObserved ready replica count, read from the Deployment provider-kubernetes writes back โ€” not the desired count from spec.
status.darlane.trafficModestringResolved routing for the twin: none, weighted (TraefikService split), header (header-matched IngressRoute), or both.
status.darlane.ttlstringThe configured darlane.ttl duration (e.g. "4h"), echoed as-is โ€” not an expiry timestamp. The composition has no clock; combine with the Deployment's creationTimestamp to compute one. Empty when unset.
status.darlane.serviceAccountNamestringName of the ServiceAccount the twin runs as, published so RBAC can be bound to it from the GitOps repo โ€” this repo's compositions never emit Role/RoleBinding themselves. Empty when no ServiceAccount is configured.

Minimal exampleโ€‹

apiVersion: platform.wxops.cloud/v1alpha1
kind: XTenantApp
metadata:
name: payment-api
spec:
parameters:
appName: payment-api
namespace: tenant-wxops
environment: dev
image: gitea.example.com/rocket-team/payment-api:dev-2025-01-15_10-30-00-abc1234
replicas: 2
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
secretsFrom:
app:
enabled: true # envFrom โ†’ payment-api-env
database:
enabled: true # envFrom โ†’ payment-api-db-creds
ingress:
enabled: true
host: payment-api.dev.example.com
tls:
enabled: true
clusterIssuer: letsencrypt-prod
darlane:
enabled: true
trafficWeight: 10 # 10% of real traffic to debug pod

See alsoโ€‹