Component Handlers
OAM Built-in Component Handlers
Package components implements oam.ComponentHandler for the built-in component
types. Each handler parses a typed config from a component’s properties and
produces the corresponding Kubernetes resources via kure’s builders. Handlers are
registered with the transformer in pkg/cmd/kurel (newBuiltinTransformer), each
mapping a component type string to a handler implementing CanHandle +
ToApplicationConfig. Every built-in component handler also implements
oam.PropertySchemaProvider (PropertySchema()), declaring a constrained schema for its
user-facing properties so the downstream runtime can validate them before invocation. Nested
Kubernetes shapes are modeled field-by-field at full fidelity — closed objects, enums where the
API has them, and the same value validation real admission applies (ADR-036 L1: one PodSpec/
Container projection shared by every kind). Only genuine escape-hatch fields (passthrough.object,
manifests/crd inline content) and key→value maps whose keys are data (nodeSelector,
resources.requests/limits) stay open by design; the remaining open objects (probes,
lifecycle, volumes, the volumeMounts items inside an initContainers/sidecars
entry) are a known gap, not the target shape; the
volumeDevices items beside them, and a sidecar’s ports items (go-kure/launcher#660), are
closed, and so is the four-key affinity shorthand: its schema declares the four keys and
no other, and since go-kure/launcher#790 its parser refuses any other key too. The
initContainers/sidecars entries themselves are closed (go-kure/launcher#321, see “Common
config”). The raw corev1 affinity that deployment publishes is
a different schema and is not part of that gap — it is modeled field-by-field. helmrelease
takes the other route to the same strictness: its schema declares exactly the top-level
HelmReleaseSpec keys and leaves the nested Flux blocks open, and its handler decodes the
whole property map strictly into HelmReleaseSpec, refusing an unknown or wrongly typed key at
any depth (see its entry under “Per-type highlights”). Every property
(including nested object fields and array item
schemas at every depth) carries a Description, surfaced in the downstream runtime’s generated Handler API
Reference.
The kind-named Flux source components (helmrepository, ocirepository, gitrepository,
bucket, helmchart) reach the same strictness another way: each schema declares exactly the top-level keys
of its source-controller spec type and leaves the nested Flux blocks open, and each handler
decodes the whole property map strictly into that type, refusing an unknown or wrongly typed key
at any depth (see their entry under “Per-type highlights”). fluxcd-kustomization does the same
with kustomize-controller’s KustomizationSpec.
Every built-in component handler and component lowering rule implements
oam.ContractDescriber (go-kure/launcher#789): family is the component type, version is
builtin.ContractVersion (v1alpha1), no capability key is required and none is deprecated.
The four rules (webservice, worker, helm, postgresql) also implement
oam.LoweringTargetDeclarer, naming every type they lower into:
| Rule | Components | Traits | Policies |
|---|---|---|---|
webservice | deployment, service, serviceaccount | topology-spread, pvc | |
worker | deployment, serviceaccount | topology-spread, pvc | |
helm | helmrelease, helmtemplate, helmrepository, ocirepository, gitrepository, bucket | configmap, secret | |
postgresql | cnpg-cluster, cnpg-objectstore, cnpg-pooler, cnpg-database | dependency, placement |
Transformer.Seal, which every transform runs first, refuses a registry that holds a rule
without all of its targets, whatever the document: registry incomplete: lowering rule component/webservice@v1alpha1 lowers into component type "service", which is not registered.
Breaking library change: a consumer that registers one of these rules with only the
handlers its own documents reach (a postgresql without the pooler, say) now fails at the
first transform, and registers the rest. See Contract metadata in
pkg/oam.
Traits a lowering rule forwards
A rule that emits several components decides which of them carries each authored trait, and a trait acts where it is carried: on that member’s objects, or on the bundle that member’s group is applied in. The rules of this package forward by what a trait covers (go-kure/launcher#794, item 8):
- A trait that covers every object the component generates (
prune-protection,force-replace: a delivery intent on the application of the member that carries it) goes to every member the rule emits for that component: each member ofwebserviceandworker, the Kustomization and the own source ofoci, the Cluster and every other member ofpostgresql. A sourcehelmgenerates, and a source severalocicomponents share, belongs to the document and carries no trait. - A trait that configures how a bundle is delivered (
fluxcd-patches,fluxcd-postbuild: not built in; a consumer that delivers through Flux registers them) goes to one member per bundle, never to two members of one group, so its handler sees it once per bundle.webservice,workerand anociwith its own source emit one same-name sibling group, which is one bundle: thedeploymentmember carries the trait for the first two, the Kustomization for the third.helm, and anocion a shared source, emit one component beside the source, and that component carries it. - Any other trait goes to the one member whose objects or contracts it acts on; each rule’s entry under “Per-type highlights” says which.
A rule whose members can land in different groups forwards a bundle trait only where it can tell the groups, and refuses the document where it cannot. Groups are computed after lowering has settled, so a rule cannot ask which bundle a member will be applied in. Applying a forwarded trait once per resulting bundle, whatever the grouping turns out to be, is not offered: it would have to happen in the engine, after grouping, and the refusal was kept instead.
postgresql is the one built-in case. Its Cluster carries every authored trait. Unless
the document orders its components (a dependency policy with a rule), the other
members stay in the Cluster’s bundle, which the Cluster’s copy configures. When it
does, the Pooler and the Databases are ordered after the Cluster, wait for nothing else
and are placed where it is, so they share one later group, and the first of them carries
a second copy for that group’s bundle. A policy of the document that orders or places
one of those members on its own (a dependency rule or a placement whose component
is a generated name) could move it to a group neither copy reaches, so a document that
authors one of the two traits on the postgresql component and holds such a policy is
refused, with this cause:
A policy that makes another component wait for a member separates nothing and is left alone. A rule an extension registers owes the same: one copy per bundle where the members provably share a group, a refusal naming the trait and the policy where a document could separate them.
How to read the wrong-type notes below
This document states wrong-type handling per field, and makes no blanket
guarantee across the package. Where a field says a present-but-wrong-type value
is rejected, that is a claim about the helper named beside it —
parseStringField, parseBoolField, parseInt32Field, parseInt64Field,
parseObjectField, parseStringList — each of which reports presence
separately from value and returns an error naming the field. A parser that
does not use one of those helpers may still reject a wrong type on its own —
several do, with their own error — so its behavior is left unspecified here.
Some fields may still fall through a bare type assertion and silently
discard a wrongly typed value instead of erroring. The shared
parseAffinity’s four sub-fields (enablePodAntiAffinity, topologyKey,
podAntiAffinityType, nodeSelector) were the documented example until
go-kure/launcher#452 moved them onto the helpers; the absence of a named
example is not a claim that none remain. Do not generalize a rejection note
from one field to its neighbours: adjudicate against the parser that actually
reads it.
Component types
type | Produces | Summary |
|---|---|---|
alertmanager | Alertmanager | Kind-named Prometheus operator Alertmanager: the whole AlertmanagerSpec, strictly decoded; no top-level field is required. The operator runs the pods: what the spec says of them (image, replicas, resources, storage, containers, initContainers, volumes, securityContext, hostNetwork) is held to the environment policy as a workload’s is, and the deprecated baseImage, tag and sha are not in the schema, and are refused when not empty (an empty one writes nothing where the properties are not first validated against the schema, which refuses all three). The replica count and memory request the operator fills where they are unset are held, not written. An image the operator chooses, for alertmanager or one of its two reloaders, is refused under a policy with allowed registries. No capability is required — see below. |
apiservice | APIService | Kind-named APIService: the whole APIServiceSpec, strictly decoded; group, version, groupPriorityMinimum and versionPriority are required, and a service names its namespace and name. The object must be named <version>.<group>, by the component’s name or objectName. It hands a group and version of the API to the Service it names, which is not resolved at build. Cluster-scoped; only the environment policy’s object kind rules apply — see below. |
artifactgenerator | ArtifactGenerator | Kind-named Flux ArtifactGenerator: the whole ArtifactGeneratorSpec, strictly decoded; sources, each with its alias, kind and name, and artifacts, each with its name and a copy of from and to, are required. A source may be one of another namespace, whose content the generator copies into its artifacts, and nothing gates it. The API’s expression rule is not checked. No environment policy applies — see below. |
backendtlspolicy | BackendTLSPolicy | Kind-named Gateway API BackendTLSPolicy: the whole BackendTLSPolicySpec, strictly decoded; at least one of targetRefs, and validation with its hostname, are required. No capability is required and no environment policy applies — see below. |
bucket | Bucket | Kind-named: the full Flux BucketSpec. |
certificate | Certificate | Kind-named cert-manager Certificate: the whole CertificateSpec, strictly decoded; secretName and issuerRef with its name are required. A keystore password written into the object is refused under an environment policy that forbids explicit secrets. The issuer is the author’s, and no capability is required. Shares its name with the certificate trait — see below. |
cilium-bgpadvertisement | CiliumBGPAdvertisement | Kind-named Cilium BGP advertisement: the whole CiliumBGPAdvertisementSpec (advertisements, required), strictly decoded; an entry’s advertisementType is required, and its service, interface and selector are held to the type. Cluster-scoped; no environment policy applies and no capability is required — see below. |
cilium-bgpclusterconfig | CiliumBGPClusterConfig | Kind-named Cilium BGP cluster configuration: the whole CiliumBGPClusterConfigSpec (nodeSelector, bgpInstances), strictly decoded; bgpInstances, an instance’s name and a peer’s name are required. The node selector is the author’s. Cluster-scoped; no environment policy applies and no capability is required — see below. |
cilium-bgpnodeconfigoverride | CiliumBGPNodeConfigOverride | Kind-named Cilium BGP per-node override: the whole CiliumBGPNodeConfigOverrideSpec (bgpInstances, required), strictly decoded; an instance’s name and a peer’s name are required. It overrides the CiliumBGPNodeConfig of the same name: objectName sets that name. Cluster-scoped; no environment policy applies and no capability is required — see below. |
cilium-bgppeerconfig | CiliumBGPPeerConfig | Kind-named Cilium BGP peer configuration: the whole CiliumBGPPeerConfigSpec (transport, timers, authSecretRef, gracefulRestart, ebgpMultihop, families), strictly decoded; no top-level field is required. Cluster-scoped; no environment policy applies and no capability is required — see below. |
cilium-cidrgroup | CiliumCIDRGroup | Kind-named Cilium CIDR group: the whole CiliumCIDRGroupSpec (externalCIDRs, required), strictly decoded. A Cilium network policy refers to the group by its name: objectName sets that name. Cluster-scoped; no environment policy applies and no capability is required — see below. |
cilium-clusterwidenetworkpolicy | CiliumClusterwideNetworkPolicy | Kind-named Cilium cluster-wide network policy: spec, specs or both, each a Cilium rule, strictly decoded as the cilium-networkpolicy kind decodes them. A rule takes exactly one of endpointSelector and nodeSelector, and at least one of ingress, ingressDeny, egress and egressDeny. Cluster-scoped; no environment policy applies and no capability is required — see below. |
cilium-egressgatewaypolicy | CiliumEgressGatewayPolicy | Kind-named Cilium egress gateway policy: the whole CiliumEgressGatewayPolicySpec (selectors, destinationCIDRs, excludedCIDRs, egressGateway, egressGateways), strictly decoded; selectors, destinationCIDRs, egressGateway and a gateway’s nodeSelector are required. The selectors are the author’s. Cluster-scoped; no environment policy applies and no capability is required — see below. |
cilium-loadbalancerippool | CiliumLoadBalancerIPPool | Kind-named Cilium load balancer address pool: the whole CiliumLoadBalancerIPPoolSpec (serviceSelector, allowFirstLastIPs, blocks, disabled), strictly decoded; no top-level field is required. The Service selector is the author’s. Cluster-scoped; no environment policy applies and no capability is required — see below. |
cilium-localredirectpolicy | CiliumLocalRedirectPolicy | Kind-named Cilium local redirect policy: the whole CiliumLocalRedirectPolicySpec (redirectFrontend, redirectBackend, skipRedirectFromBackend, description), strictly decoded; the frontend, the backend, its selector and its ports are required, and the frontend takes exactly one of addressMatcher and serviceMatcher. Namespaced; no environment policy applies and no capability is required — see below. |
cilium-networkpolicy | CiliumNetworkPolicy | Kind-named CiliumNetworkPolicy: its spec (one rule) and specs (a list of rules), each the whole Cilium rule, strictly decoded. At least one is required; each rule needs an endpointSelector and an entry in ingress, ingressDeny, egress or egressDeny, and nodeSelector is refused. An authored object, not the cilium-networkpolicy trait; no environment policy applies — see below. |
cilium-nodeconfig | CiliumNodeConfig | Kind-named Cilium per-node configuration: the whole CiliumNodeConfigSpec (defaults, nodeSelector, both required), strictly decoded. The keys and values of defaults are written as authored. Namespaced; no environment policy applies and no capability is required — see below. |
clusterexternalsecret | ClusterExternalSecret | Kind-named External Secrets Operator ClusterExternalSecret: the whole ClusterExternalSecretSpec, strictly decoded; externalSecretSpec is required, with what an externalsecret requires and what it refuses of a target.manifest under the environment policy. Cluster-scoped; no capability is required — see below. |
clusterissuer | ClusterIssuer | Kind-named cert-manager ClusterIssuer: the IssuerSpec of issuer, strictly decoded, and its policy check. Cluster-scoped. No capability is required — see below. |
clusterrole | ClusterRole | Kind-named RBAC ClusterRole: the object’s own fields (rules, aggregationRule), strictly decoded; a rule’s verbs are required, and either its apiGroups and resources or its nonResourceURLs; an aggregationRule needs a selector. Cluster-scoped. Ungated: no capability and no environment-policy check restricts what a role grants — see below. |
clusterrolebinding | ClusterRoleBinding | Kind-named RBAC ClusterRoleBinding: the object’s own fields (subjects; roleRef, required with its kind and name), strictly decoded; a subject’s kind and name are required, and a ServiceAccount subject’s namespace. Cluster-scoped. Ungated: no capability and no environment-policy check restricts what a role grants — see below. |
clustersecretstore | ClusterSecretStore | Kind-named External Secrets Operator ClusterSecretStore: the SecretStoreSpec of secretstore, strictly decoded, and its policy check. Cluster-scoped. No capability is required — see below. |
cnpg-backup | CNPG Backup | Kind-named CloudNativePG Backup: the whole BackupSpec, strictly decoded; cluster.name is required. A one-shot request with a spec the API never lets change: a backup that recurs is a cnpg-scheduledbackup. Namespaced; no environment policy applies and no capability is required — see below. |
cnpg-cluster | CNPG Cluster | Operator-CR kind component: the whole postgresql.cnpg.io/v1 ClusterSpec, strictly decoded, with no launcher opinions — see below. |
cnpg-clusterimagecatalog | CNPG ClusterImageCatalog | Kind-named CloudNativePG cluster-wide image catalog: the same ImageCatalogSpec, strictly decoded, and the same policy check and tag rule. Cluster-scoped. No capability is required — see below. |
cnpg-database | CNPG Database | Operator-CR kind component: the whole DatabaseSpec, strictly decoded — see below. |
cnpg-databaserole | CNPG DatabaseRole | Kind-named CloudNativePG DatabaseRole: the whole DatabaseRoleSpec, strictly decoded; cluster.name and name are required, and the names the CRD reserves, ensure: absent, a password Secret beside disablePassword: true and a client certificate without login: true are refused. Its password is a Secret’s name, never a value. Namespaced; no environment policy applies and no capability is required — see below. |
cnpg-imagecatalog | CNPG ImageCatalog | Kind-named CloudNativePG image catalog: the whole ImageCatalogSpec (images, required, and componentImages), strictly decoded; an image’s image and major and a component image’s key and image are required. Every image the catalog names is held to the environment policy’s allowed registries and, with or without a policy, to the tag rule (a tag or a digest, no :latest). Namespaced; no capability is required — see below. |
cnpg-objectstore | Barman Cloud ObjectStore | Operator-CR kind component: the whole barmancloud.cnpg.io/v1 ObjectStoreSpec, strictly decoded — see below. |
cnpg-pooler | CNPG Pooler | Operator-CR kind component: the whole PoolerSpec, strictly decoded — see below. |
cnpg-publication | CNPG Publication | Kind-named CloudNativePG Publication: the whole PublicationSpec, strictly decoded; cluster.name, name, dbname and target are required, and the target publishes all tables or a list of objects, not both. Namespaced; no environment policy applies and no capability is required — see below. |
cnpg-scheduledbackup | CNPG ScheduledBackup | Kind-named CloudNativePG ScheduledBackup: the whole ScheduledBackupSpec, strictly decoded; schedule and cluster.name are required. The schedule is the operator’s to read. Namespaced; no environment policy applies and no capability is required — see below. |
cnpg-subscription | CNPG Subscription | Kind-named CloudNativePG Subscription: the whole SubscriptionSpec, strictly decoded; cluster.name, name, dbname, publicationName and externalClusterName are required. Namespaced; no environment policy applies and no capability is required — see below. |
configmap | ConfigMap | Kind-named ConfigMap: data, binaryData, immutable. A workload reads it through a configMap volume or envFrom — see below. |
crd | CustomResourceDefinition(s) | CRDs from inline/url; rejects non-CRD docs. |
cronjob | CronJob | Scheduled job; cron schedule + history limits + CronJobSpec/JobSpec fields, plus the raw affinity/tolerations/topologySpreadConstraints (see below). |
csidriver | CSIDriver | Kind-named CSIDriver: the whole CSIDriverSpec, strictly decoded. Cluster-scoped, and the object’s name (the component’s, or its objectName) is the driver’s name; no environment policy applies — see below. |
daemonset | DaemonSet | Per-node daemon; honors tolerations, the raw affinity and topologySpreadConstraints, and takes sidecars. Emits no Service (go-kure/launcher#690). |
deployment | Deployment | Kind-named Deployment: the shared container and pod surface, the rest of DeploymentSpec, the main container’s ports, and the raw corev1 affinity/tolerations/topologySpreadConstraints. Not a superset of worker — see below. |
endpointslice | EndpointSlice | Kind-named EndpointSlice: the object’s own fields (addressType, required; endpoints; ports), strictly decoded; an endpoint’s addresses are required. It belongs to a Service through the kubernetes.io/service-name label, authored under labels as a literal that does not follow a Service’s objectName. Namespaced; no environment policy applies and no capability is required — see below. |
externalsecret | ExternalSecret | Kind-named External Secrets Operator ExternalSecret: the whole ExternalSecretSpec (secretStoreRef, target, refreshPolicy, refreshInterval, syncWindows, data, dataFrom), strictly decoded; no top-level field is required. The store is the author’s. The environment policy reaches one field: a target.manifest of a kind the policy checks is refused. No capability is required. Beside the external-secret trait — see below. |
fluxcd-alert | Alert | Kind-named Flux Alert: the whole AlertSpec, strictly decoded; providerRef with its name and eventSources, each with its kind and name, are required. A source may name the objects of another namespace, and nothing gates it. No environment policy applies — see below. |
fluxcd-kustomization | Kustomization | Kind-named: the full Flux KustomizationSpec, against an existing source; what oci lowers to beside an ocirepository (OCIRule, see below). An authored Flux object, not how an application is delivered. |
fluxcd-provider | Provider | Kind-named Flux Provider: the whole ProviderSpec, strictly decoded; type is required, and of an authored Secret reference its name. A user or a password in address or proxy is refused under every policy and under none, and an address that is itself the credential under a policy that forbids explicit secrets; the hosts of both are written as authored, and nothing gates serviceAccountName — see below. |
fluxcd-receiver | Receiver | Kind-named Flux Receiver: the whole ReceiverSpec, strictly decoded; type and resources, each with its kind and name, are required, and of an authored secretRef its name. The Receiver opens an inbound path on the notification controller, and a resource may name the objects of another namespace; nothing gates either. The API’s expression rules are not checked. No environment policy applies — see below. |
gateway | Gateway | Kind-named Gateway API Gateway: the whole GatewaySpec, strictly decoded; gatewayClassName and listeners are required, and of a listener its name, port and protocol. It is not the Gateway a capability names for the httproute trait. No capability is required and no environment policy applies — see below. |
gatewayclass | GatewayClass | Kind-named Gateway API GatewayClass: the whole GatewayClassSpec (controllerName, required, parametersRef and description), strictly decoded. Cluster-scoped. No capability is required and no environment policy applies — see below. |
gitrepository | GitRepository | Kind-named: the full Flux GitRepositorySpec. |
grpcroute | GRPCRoute | Kind-named GRPCRoute: the whole GRPCRouteSpec (parentRefs, useDefaultGateways, hostnames, rules), strictly decoded, on the httproute kind’s recipe: nothing required or filled, no NetworkPolicy allow rule and no environment policy — see below. |
helm | via helmrelease (+ a values configmap trait, a secretValues secret trait) + a generated helmrepository/ocirepository/gitrepository/bucket, or via helmtemplate | Role-named Helm component: Flux (flux) or client-side template delivery. Lowered to the kind-named terminals (HelmRule), sharing one generated source per content identity within a document. See below. |
helmchart | HelmChart | Kind-named: the full Flux HelmChartSpec, a chart from an existing source. Not the composite removed under this name (go-kure/launcher#350); that is helm. |
helmrelease | HelmRelease | Kind-named: the full Flux HelmReleaseSpec, against an existing source. |
helmrepository | HelmRepository | Kind-named: the full Flux HelmRepositorySpec, and nothing else. |
helmtemplate | rendered manifests | Kind-named client-side Helm render: source.url, chart, version, values, secretValues, scopeOverrides. What helm lowers to under delivery: template, authorable directly. The source host and every rendered workload are checked against the environment policy — see below. |
horizontalpodautoscaler | HorizontalPodAutoscaler | Kind-named HorizontalPodAutoscaler: the whole HorizontalPodAutoscalerSpec, strictly decoded; scaleTargetRef and maxReplicas are required. maxReplicas is held to the environment policy’s replica maximum, no default filled; the target is the author’s and is not checked — see below. |
httproute | HTTPRoute | Kind-named HTTPRoute: the whole HTTPRouteSpec (parentRefs, useDefaultGateways, hostnames, rules), strictly decoded. An authored object, not the httproute trait: no parent is synthesized from a capability, no NetworkPolicy allow rule is synthesized for it and no environment policy applies — see below. |
imagepolicy | ImagePolicy | Kind-named Flux ImagePolicy: the whole ImagePolicySpec, strictly decoded; imageRepositoryRef with its name and policy are required, and of a semver policy its range. The policy may name the ImageRepository of another namespace, and nothing gates it. The API’s expression rules are not checked. No environment policy applies — see below. |
imagerepository | ImageRepository | Kind-named Flux ImageRepository: the whole ImageRepositorySpec, strictly decoded; image and interval are required, and of an authored Secret reference its name. The registry of image is held to the environment policy’s allowed registries; the tag rule is not applied, and nothing gates accessFrom, serviceAccountName or insecure — see below. |
imageupdateautomation | ImageUpdateAutomation | Kind-named Flux ImageUpdateAutomation: the whole ImageUpdateAutomationSpec, strictly decoded; sourceRef with its kind and name and interval are required, and of an authored git its commit with the author’s email. The automation commits and pushes to the repository of the GitRepository it names, which may be one of another namespace, and nothing gates it. No environment policy applies — see below. |
ingress | Ingress | Kind-named Ingress: the whole IngressSpec (ingressClassName, defaultBackend, tls, rules), strictly decoded. An authored object, not the ingress trait: no NetworkPolicy allow rule is synthesized for it and no environment policy applies — see below. |
ingressclass | IngressClass | Kind-named IngressClass: the whole IngressClassSpec (controller, required, and parameters), strictly decoded. Cluster-scoped; no environment policy applies — see below. |
issuer | Issuer | Kind-named cert-manager Issuer: the whole IssuerSpec (acme, ca, vault, selfSigned, venafi), strictly decoded; no top-level field is required. The cpu and memory of an ACME HTTP01 solver’s pod template are held to the environment policy’s maxima. No capability is required — see below. |
job | Job | Run-to-completion workload; the same JobSpec fields as cronjob’s job template, plus its own suspend and the raw affinity/tolerations/topologySpreadConstraints (see below). |
limitrange | LimitRange | Kind-named LimitRange: the whole LimitRangeSpec (limits, required), strictly decoded — see below. |
listenerset | ListenerSet | Kind-named Gateway API ListenerSet: the whole ListenerSetSpec, strictly decoded; parentRef with its name and at least one of listeners, each with its name, port and protocol, are required. No capability is required and no environment policy applies — see below. |
manifests | any | Raw manifests from inline/url with namespace stamping + scopeOverrides. Every object is checked against the environment policy — see below. |
metallb-bfdprofile | BFDProfile | Kind-named MetalLB BFD profile: the whole BFDProfileSpec (receiveInterval, transmitInterval, echoInterval, detectMultiplier, echoMode, passiveMode and minimumTtl, none required), strictly decoded. It is the timers of the BFD session of the BGP peers that name it, and so how fast the loss of such a peer is noticed. Namespaced; no environment policy applies and no capability is required — see below. |
metallb-bgpadvertisement | BGPAdvertisement | Kind-named MetalLB advertisement over BGP: the whole BGPAdvertisementSpec (ipAddressPools, ipAddressPoolSelectors, peers, nodeSelectors, serviceSelectors, aggregationLength, aggregationLengthV6, localPref and communities, none required), strictly decoded. It says which pools’ addresses MetalLB announces to which BGP peers, for which Services, and with which route attributes; one that authors nothing limits none of them. serviceSelectors is refused beside an aggregation length other than 32 (IPv4) or 128 (IPv6), the CRD’s expression rule. Namespaced; no environment policy applies and no capability is required — see below. |
metallb-bgppeer | BGPPeer | Kind-named MetalLB BGP peer, at metallb.io/v1beta2: the whole BGPPeerSpec (myASN, required; the peer’s AS number, its address or interface, its port and timers, nodeSelectors, password, passwordSecret, bfdProfile and the switches of the session), strictly decoded. It is a router the cluster’s nodes hold a BGP session with, and so one MetalLB announces addresses to. A connectTime outside 1 to 65535 seconds, or not a whole number of seconds read in whole milliseconds, is refused, the CRD’s two expression rules on a create, and so is a peerPort: 0. A password written into the object is refused under an environment policy that forbids explicit secrets. Namespaced; no capability is required — see below. |
metallb-community | Community | Kind-named MetalLB Community: the whole CommunitySpec (communities, a list of aliases of a name and a value, none required), strictly decoded. It gives names to BGP community values; a BGP advertisement that names one attaches its value to what it announces. Namespaced; no environment policy applies and no capability is required — see below. |
metallb-ipaddresspool | IPAddressPool | Kind-named MetalLB address pool: the whole IPAddressPoolSpec (addresses, required, autoAssign, avoidBuggyIPs and serviceAllocation), strictly decoded. It says which Services, in which namespaces, MetalLB gives an address of which range. Namespaced; no environment policy applies and no capability is required — see below. |
metallb-l2advertisement | L2Advertisement | Kind-named MetalLB advertisement on the local network: the whole L2AdvertisementSpec (ipAddressPools, ipAddressPoolSelectors, nodeSelectors, interfaces and serviceSelectors, none required), strictly decoded. It says which pools’ addresses MetalLB announces on the local network, from which nodes and interfaces, for which Services; one that authors nothing limits none of them. Namespaced; no environment policy applies and no capability is required — see below. |
mutatingwebhookconfiguration | MutatingWebhookConfiguration | Kind-named MutatingWebhookConfiguration: its webhooks, strictly decoded; a webhook’s name, clientConfig (exactly one of url and service), sideEffects and admissionReviewVersions are required, and a rule names its operations, API groups, versions and resources. It changes objects of any namespace after the build. Cluster-scoped; only the environment policy’s object kind rules apply — see below. |
namespace | Namespace | Kind-named Namespace: the whole NamespaceSpec (finalizers), strictly decoded. Cluster-scoped, named after the component; its labels are the labels property — see below. |
networkpolicy | NetworkPolicy | Kind-named NetworkPolicy: the whole NetworkPolicySpec (podSelector, ingress, egress, policyTypes), strictly decoded. An authored object, not the networkpolicy trait: nothing scopes it to a component’s pods, so an unwritten podSelector selects every pod of the namespace; no policyTypes are derived; a defective match expression of a selector is refused; no environment policy applies — see below. |
oci | OCIRepository, Kustomization | Sync manifests from an OCI artifact (Flux). |
ocirepository | OCIRepository | Kind-named: the full Flux OCIRepositorySpec, with no Kustomization (compare oci). |
passthrough | any (verbatim) | Emit one arbitrary object as-declared; a built-in kind has the scope the Kubernetes API gives it, any other kind the scope an authored clusterScoped states, or else the one registered for it (namespaced when unknown); a list is rejected, a workload, claim or autoscaler is held to the environment policy, and a Secret is refused under a policy that forbids explicit secrets. |
persistentvolume | PersistentVolume | Kind-named PersistentVolume: the whole PersistentVolumeSpec, its volume sources included, strictly decoded. Cluster-scoped. A hostPath or local source and capacity.storage are held to environment policy — see below. |
persistentvolumeclaim | PersistentVolumeClaim | Kind-named claim: size, storageClassName, accessModes, volumeMode, selector, dataSourceRef, volumeName, volumeAttributesClassName. A workload mounts it with a pvc volume’s claimName — see below. |
pod | Pod | Kind-named bare Pod: the whole PodSpec less ephemeralContainers, priority and overhead, strictly decoded. Held to environment policy as a rendered Pod is; no default filled. Carries the app label, so traits and Services select it — see below. |
poddisruptionbudget | PodDisruptionBudget | Kind-named PodDisruptionBudget: the whole PodDisruptionBudgetSpec (minAvailable, maxUnavailable, selector, unhealthyPodEvictionPolicy), strictly decoded; no top-level field is required and the selector is the author’s, a defective match expression of it refused. No environment policy applies — see below. |
podmonitor | PodMonitor | Kind-named Prometheus operator PodMonitor: the whole PodMonitorSpec, strictly decoded; selector is required. The selector is the author’s. No environment policy applies and no capability is required — see below. |
podtemplate | PodTemplate | Kind-named PodTemplate: its one field, template, strictly decoded into PodTemplateSpec. The pod spec is held to what the pod kind holds its own to and to environment policy; activeDeadlineSeconds is allowed, no default filled. Stored, not run: no app label and not a trait target — see below. |
postgresql | CNPG Cluster, Pooler, ObjectStore, Database | CloudNativePG database (backup/monitoring/pooling). |
priorityclass | PriorityClass | Kind-named PriorityClass: value (0 when unauthored), globalDefault, description, preemptionPolicy, strictly decoded. Cluster-scoped; no environment policy applies — see below. |
prometheus | Prometheus | Kind-named Prometheus operator Prometheus: the whole PrometheusSpec, strictly decoded, every field under its upstream name, externalLabels included; no top-level field is required. The operator runs the pods: image and thanos.image, replicas times shards, resources and thanos.resources, storage, containers, initContainers, volumes, securityContext and hostNetwork are held to the environment policy as the Alertmanager’s are, the deprecated baseImage, tag and sha are refused (the spec’s when not empty, an empty one writing nothing; the sidecar’s whenever set, the empty string included), and a literal bearerToken of remoteWrite[], remoteRead[] or apiserverConfig is refused under a policy that forbids explicit secrets. The pod count the operator runs, an unset replica or shard count read as 1, is held, not written. An image the operator chooses, for prometheus, its two reloaders or, where thanos is set, thanos-sidecar, is refused under a policy with allowed registries. No capability is required — see below. |
prometheus-probe | Probe | Kind-named Prometheus operator Probe: the whole ProbeSpec, strictly decoded; prober.url is required. The prober and the targets are the author’s. No environment policy applies and no capability is required — see below. |
prometheusrule | PrometheusRule | Kind-named Prometheus operator PrometheusRule: the whole PrometheusRuleSpec (groups), strictly decoded; a group’s name and a rule’s expr are required. No environment policy applies and no capability is required — see below. |
referencegrant | ReferenceGrant | Kind-named Gateway API ReferenceGrant: the whole ReferenceGrantSpec, strictly decoded; from and to are required, with the group, kind and namespace of a source and the group and kind of a target. No capability is required and no environment policy applies — see below. |
replicaset | ReplicaSet | Kind-named bare ReplicaSet: the whole ReplicaSetSpec, strictly decoded; selector and template are required. The pod template is held to what the pod kind holds its spec to, less activeDeadlineSeconds, and gains the app label; replicas and the template are held to environment policy, no default filled — see below. |
replicationcontroller | ReplicationController | Kind-named bare ReplicationController: the whole ReplicationControllerSpec, strictly decoded; template is required, selector (a plain label map) optional. The pod template and replicas are held as the replicaset kind’s are, activeDeadlineSeconds refused included, and the template gains the app label — see below. |
replicationdestination | ReplicationDestination | Kind-named VolSync ReplicationDestination: the whole ReplicationDestinationSpec (trigger, the movers rsync, rsyncTLS, rclone and restic, external, paused), strictly decoded, with the policy checks of replicationsource. No top-level field is required and no capability is required — see below. |
replicationsource | ReplicationSource | Kind-named VolSync ReplicationSource: the whole ReplicationSourceSpec (sourcePVC, trigger, the movers rsync, rsyncTLS, rclone, restic and syncthing, external, paused), strictly decoded; no top-level field is required. An authored capacity is held to the environment policy’s storage maximum, a mover’s cpu and memory to its maxima, and a mover’s hostProcess switch is refused unless privileged workloads are allowed. An rsync mover is held to the policy’s container capabilities for the seven the linked operator version adds to its container. No capability is required. Its object is of the kind the volsync trait builds — see below. |
resourcequota | ResourceQuota | Kind-named ResourceQuota: the whole ResourceQuotaSpec (hard, scopes, scopeSelector), strictly decoded — see below. |
resourcesetinputprovider | ResourceSetInputProvider | Kind-named Flux Operator ResourceSetInputProvider: the whole ResourceSetInputProviderSpec, strictly decoded; type is required, of an authored Secret reference its name and of a schedule its cron. A user or a password in url is refused under every policy and under none, and the host of url is held to the environment policy’s allowed registries, whatever the type. An ExternalArtifact provider’s selectors may list the ExternalArtifacts of another namespace or of every namespace, and nothing gates them or serviceAccountName. The API’s expression rules are not checked — see below. |
role | Role | Kind-named RBAC Role: the object’s own field (rules), strictly decoded; a rule’s verbs, apiGroups and resources are required, and nonResourceURLs are refused. Namespaced. Ungated: no capability and no environment-policy check restricts what a role grants — see below. |
rolebinding | RoleBinding | Kind-named RBAC RoleBinding: the object’s own fields (subjects; roleRef, required with its kind and name), strictly decoded; a subject’s kind and name are required. The names of the role and of the subjects are the author’s literals. Namespaced. Ungated: no capability and no environment-policy check restricts what a role grants — see below. |
runtimeclass | RuntimeClass | Kind-named RuntimeClass: handler (required), overhead, scheduling, strictly decoded. Cluster-scoped; no environment policy applies — see below. |
secret | Secret | Kind-named Secret: stringData, data, type, immutable. Every entry is emitted under data, base64-encoded and not encrypted. Refused under an environment policy that forbids explicit secrets. A workload reads it through a secret volume, envFrom or a secretKeyRef — see below. |
secretstore | SecretStore | Kind-named External Secrets Operator SecretStore: the whole SecretStoreSpec (provider, controller, retrySettings, refreshInterval, conditions), strictly decoded; provider with exactly one provider is required, and of that provider what the API requires. A credential written into the object is refused under an environment policy that forbids explicit secrets. No capability is required — see below. |
service | Service | Kind-named Service in front of pods another component owns: selector, the full ports list (with nodePort and appProtocol), all four type values, clusterIP: None for a headless one, externalName, the traffic policies, session affinity, the IP families and the load-balancer fields. Emits nothing else — see below. |
serviceaccount | ServiceAccount | Kind-named ServiceAccount: automountServiceAccountToken, imagePullSecrets. A workload names it with serviceAccountName — see below. |
servicecidr | ServiceCIDR | Kind-named ServiceCIDR: the whole ServiceCIDRSpec (cidrs, required, at least one), strictly decoded. Cluster-scoped; no environment policy applies — see below. |
servicemonitor | ServiceMonitor | Kind-named Prometheus operator ServiceMonitor: the whole ServiceMonitorSpec, strictly decoded; endpoints and selector are required. The selector is the author’s. No environment policy applies and no capability is required — see below. |
statefulset | StatefulSet | Stateful workload with volumeClaimTemplates; serviceName names a governing service authored beside it. Emits no Service (go-kure/launcher#690). |
storageclass | StorageClass | Kind-named StorageClass: the object’s fields beside its identity (provisioner, required, parameters, reclaimPolicy, …), strictly decoded. Cluster-scoped; no environment policy applies — see below. |
tcproute | TCPRoute | Kind-named Gateway API TCPRoute: the whole TCPRouteSpec, strictly decoded; rules and a rule’s backendRefs are required, with the name of a backend and the port of one that is a Service. An authored object, as the httproute kind is: no parent is synthesized from a capability, no NetworkPolicy allow rule is synthesized for it, no capability is required and no environment policy applies — see below. |
thanosruler | ThanosRuler | Kind-named Prometheus operator ThanosRuler: the whole ThanosRulerSpec, strictly decoded, with the spec’s labels (Prometheus external labels) published as externalLabels; no top-level field is required. The operator runs the pods: image, replicas, resources, storage, containers, initContainers, volumes and securityContext are held to the environment policy as the Alertmanager’s are, and a literal remoteWrite[].bearerToken under a policy that forbids explicit secrets. The replica count and memory request the operator fills where they are unset are held, not written. An image the operator chooses, for thanos-ruler or its config-reloader, is refused under a policy with allowed registries. No capability is required — see below. |
tlsroute | TLSRoute | Kind-named Gateway API TLSRoute: the whole TLSRouteSpec, strictly decoded; hostnames is required, and the rest as on a tcproute. An authored object on the same terms — see below. |
udproute | UDPRoute | Kind-named Gateway API UDPRoute: the whole UDPRouteSpec, strictly decoded; required as on a tcproute, and an authored object on the same terms — see below. |
validatingwebhookconfiguration | ValidatingWebhookConfiguration | Kind-named ValidatingWebhookConfiguration: its webhooks, strictly decoded, required as on a mutatingwebhookconfiguration. The API server calls it to admit or deny the requests of any namespace its rules match. Cluster-scoped; only the environment policy’s object kind rules apply — see below. |
volumeattributesclass | VolumeAttributesClass | Kind-named VolumeAttributesClass: driverName and parameters (both required, parameters with at least one entry), strictly decoded. Cluster-scoped; no environment policy applies — see below. |
webservice | Deployment, Service, ServiceAccount (+PVC) | HTTP service with replicas, probes, env, volumes. Lowered to a same-name deployment, service and (unless serviceAccountName is authored) serviceaccount group plus a topology-spread trait (WebserviceRule) — see below. |
worker | Deployment, ServiceAccount (+PVC) | Background workload (no Service/port). Lowered to a same-name deployment and (unless serviceAccountName is authored) serviceaccount group plus a topology-spread trait (WorkerRule) — see below. |
Only the two role kinds, webservice and worker, emit a per-component
ServiceAccount, and only when the component does not author
serviceAccountName. The five pod kinds (deployment, statefulset,
daemonset, job, cronjob) emit no ServiceAccount and no claim of their own
(go-kure/launcher#702). See “Pod-level properties” and “Referencing an existing
claim” below.
Kind inventory
Every object the base library can construct, and how a document reaches it
(go-kure/launcher#790). A row is one of the base library’s generated constructors
(pkg/kubernetes/**/zz_generated_create.go in github.com/go-kure/kure), written
<package directory>.Create<Kind>. Two tests hold the table to the code:
TestKindInventory_CoversEveryConstructorreads those files from the linked module and fails when a constructor has no row, a row has no constructor, or a row’s Kind cell is not the API version, kind and scope the constructor’s doc comment gives. A base-library bump that adds a kind, or moves one to another API version, therefore fails until the table says so.TestKindInventory_MatchesCallSitesholds the Status and Type columns to this package and../traits: akindrow’s constructor is called here, atraitrow’s there or here (a trait may build through this package, asconfigmapdoes), and amissing,heldornot authorablerow’s by neither. Akindorcomponentrow’s Type is a type a handler or lowering rule of this package declares contract metadata for, atraitrow’s one of../traits, so a row stops passing when its handler is removed.
Not held by a test: which handler makes the call, so a kind row passes while its Type has a
handler and any component of this package calls its constructor; whether a component row’s
constructor is called; the step from trait to kind, which the change that adds the kind
component makes in the row; the Decode and Notes columns, checked for presence only; and code
outside the two packages, which is not read.
Status is one of:
kind: a kind component projects the object; Type is its componenttype.component: a component that is not a kind component emits it; Notes says why there is no kind component.trait: a trait emits it and no component does; Type is the traittype.missing: authorable, with no component yet. go-kure/launcher#790 adds these group by group.held: authorable, with no component until something named is in place; Notes gives the reason. A component for it would not do what the kind is authored for, or would act where no check of the build reaches.not authorable: no component is planned; Notes gives the reason.
Cluster-wide admission and API registration (go-kure/launcher#943). The
ValidatingWebhookConfiguration, MutatingWebhookConfiguration and APIService kinds are kind
rows. No dimension of the environment policy reads what they match or where they send a
request. They are gated as every emitted object is, by the policy’s object kind rules
(ObjectKindPolicy): an environment that must not emit them forbids their group, their kind,
or cluster-scoped objects, and the build refuses them on every path. What they do once
applied is out of the build’s reach, and each kind’s entry states it:
- They act on every other document’s objects, or on the API itself. The API server applies a webhook configuration to the requests its rules match, in any namespace and from any author; an APIService hands a group and version of the API to the Service it names.
- A mutating webhook changes objects after the build, so what the build checked need not hold of the object that is stored.
- Where they send a request is not resolved at build: a webhook’s
clientConfig(a URL or a Service) and an APIService’sserviceare written as authored. - A match condition’s CEL expression is compiled by the API server, not at build.
ValidatingAdmissionPolicy and its binding, and MutatingAdmissionPolicy and its binding, are
still held: their kind components follow under go-kure/launcher#943, on the same terms.
An RBAC grant, which launcher also emits, differs: it names its subjects, and the document
that makes the grant states it. These kinds act on objects no document of the build names.
What manifests, passthrough and template delivery do with such a document is said in
their own entries.
Decode is how the properties become the object: hand-written parser (a schema and parser this
package or ../traits maintains) or a strict decode into the named upstream type, where an
unknown or wrongly typed key is an error at every depth the decoder reaches. It does not reach
inside an upstream type that unmarshals itself, where an unknown nested key is dropped unless
the row says the type is checked separately, as the CiliumNetworkPolicy row does.
| Constructor | Kind | Status | Type | Decode | Notes |
|---|---|---|---|---|---|
kubernetes.CreateAPIService | apiregistration.k8s.io/v1 APIService (cluster-scoped) | kind | apiservice | strict decode of APIServiceSpec | group, version, groupPriorityMinimum and versionPriority must be written, and a service its namespace and name. The object is named after the component unless objectName names it, and the kind refuses any name but <version>.<group>, which the API requires. It hands a group and version of the API to the Service it names, which is not resolved at build. The priorities’ ranges, the port and how caBundle and insecureSkipTLSVerify combine are left to the API server. Only the object kind rules of the environment policy apply. See “Cluster-wide admission and API registration” above. |
kubernetes.CreateBackendTLSPolicy | gateway.networking.k8s.io/v1 BackendTLSPolicy | kind | backendtlspolicy | strict decode of BackendTLSPolicySpec | targetRefs, at least one, and validation with its hostname must be written; of a target, a CA certificate reference and a subject alternative name that are authored, the fields the API requires that the type would write empty. The hostnames it validates are not held to the allowed registries. No capability is required. No environment policy applies. |
kubernetes.CreateBinding | v1 Binding | not authorable | - | - | A request body for a pod’s binding subresource, not a stored object. |
kubernetes.CreateCSIDriver | storage.k8s.io/v1 CSIDriver (cluster-scoped) | kind | csidriver | strict decode of CSIDriverSpec | The object’s name, the component’s or its objectName, is the CSI driver’s name. The API documents a limit of 63 characters for it and the API server does not hold the object to that limit. Its labels and annotations are the labels and annotations properties. No environment policy applies. |
kubernetes.CreateCSINode | storage.k8s.io/v1 CSINode (cluster-scoped) | not authorable | - | - | Written by the kubelet for the CSI drivers on its node. |
kubernetes.CreateCSIStorageCapacity | storage.k8s.io/v1 CSIStorageCapacity | not authorable | - | - | Written by a CSI driver’s provisioner. |
kubernetes.CreateClusterRole | rbac.authorization.k8s.io/v1 ClusterRole (cluster-scoped) | kind | clusterrole | strict decode of the object, less kind, apiVersion and metadata | Ungated: no capability and no environment-policy check restricts what a role grants. A rule’s verbs must be written, and either its apiGroups and resources or its nonResourceURLs; an aggregationRule needs a selector. The rbac trait emits one too, through its own hand-written parser. |
kubernetes.CreateClusterRoleBinding | rbac.authorization.k8s.io/v1 ClusterRoleBinding (cluster-scoped) | kind | clusterrolebinding | strict decode of the object, less kind, apiVersion and metadata | Ungated: no capability and no environment-policy check restricts what a role grants. roleRef with its kind and name must be written, a subject’s kind and name, and a ServiceAccount subject’s namespace. The names are the author’s literals. The rbac trait emits one too, through its own hand-written parser. |
kubernetes.CreateComponentStatus | v1 ComponentStatus (cluster-scoped) | not authorable | - | - | Read-only: the API server computes it. |
kubernetes.CreateConfigMap | v1 ConfigMap | kind | configmap | hand-written parser | The configmap trait builds through the same path. |
kubernetes.CreateControllerRevision | apps/v1 ControllerRevision | not authorable | - | - | Written by the StatefulSet and DaemonSet controllers. |
kubernetes.CreateCronJob | batch/v1 CronJob | kind | cronjob | hand-written parser | - |
kubernetes.CreateCustomResourceDefinition | apiextensions.k8s.io/v1 CustomResourceDefinition (cluster-scoped) | component | crd | the manifest parser, CustomResourceDefinition documents only | The stated exception: an application takes its CRDs from upstream files (inline or url), so no kind component projects the spec. |
kubernetes.CreateDaemonSet | apps/v1 DaemonSet | kind | daemonset | hand-written parser | - |
kubernetes.CreateDeployment | apps/v1 Deployment | kind | deployment | hand-written parser | webservice and worker lower onto it. |
kubernetes.CreateEndpointSlice | discovery.k8s.io/v1 EndpointSlice | kind | endpointslice | strict decode of the object, less kind, apiVersion and metadata | A slice belongs to a Service only through its kubernetes.io/service-name label, authored under labels as a literal that does not follow the Service’s objectName. addressType and an endpoint’s addresses must be written. The addresses and FQDNs of its endpoints are not artifact sources. No environment policy applies. |
kubernetes.CreateEndpoints | v1 Endpoints | not authorable | - | - | Not offered: deprecated upstream in favour of EndpointSlice. |
kubernetes.CreateEvent | v1 Event | not authorable | - | - | A record the system writes at run time. |
kubernetes.CreateEviction | policy/v1 Eviction | not authorable | - | - | A request body for a pod’s eviction subresource, not a stored object. |
kubernetes.CreateGRPCRoute | gateway.networking.k8s.io/v1 GRPCRoute | kind | grpcroute | strict decode of GRPCRouteSpec | On the httproute kind’s recipe: nothing required or filled, no environment policy, parent and backend references carried as authored, their namespaces included. The API requires no rule, backend or hostname of a GRPCRoute, as of an HTTPRoute, unlike the TCP, UDP and TLS routes. No type under GRPCRouteSpec unmarshals itself, so the decode reaches every depth. |
kubernetes.CreateGateway | gateway.networking.k8s.io/v1 Gateway | kind | gateway | strict decode of GatewaySpec | gatewayClassName and listeners must be written, and of a listener its name, port and protocol; of what is authored below them, the fields the API requires that the type would write empty. defaultScope is an experimental-channel field. The pods a controller starts for a Gateway are not the object’s to size. No capability is required: the Gateway an httproute trait takes from a capability is not this component. No environment policy applies. |
kubernetes.CreateGatewayClass | gateway.networking.k8s.io/v1 GatewayClass (cluster-scoped) | kind | gatewayclass | strict decode of GatewayClassSpec | controllerName must be written, and of a parametersRef that is authored its group, kind and name. No capability is required. No environment policy applies. |
kubernetes.CreateHTTPRoute | gateway.networking.k8s.io/v1 HTTPRoute | kind | httproute | strict decode of HTTPRouteSpec | No type under HTTPRouteSpec unmarshals itself, so the decode reaches every depth. The httproute trait, which expose lowers onto, builds its own HTTPRoute with a hand-written parser. Only the trait feeds the NetworkPolicy synthesis, takes its parent from a capability and is held to the policy’s capability lists. |
kubernetes.CreateHorizontalPodAutoscaler | autoscaling/v2 HorizontalPodAutoscaler | kind | horizontalpodautoscaler | strict decode of HorizontalPodAutoscalerSpec | scaleTargetRef and maxReplicas must be written. maxReplicas is held to the environment policy’s replica maximum. The scaler trait emits one for its workload too, through its own parser. |
kubernetes.CreateIPAddress | networking.k8s.io/v1 IPAddress (cluster-scoped) | not authorable | - | - | Allocated by the API server for a Service. |
kubernetes.CreateIngress | networking.k8s.io/v1 Ingress | kind | ingress | strict decode of IngressSpec | The ingress trait, which expose lowers onto, builds its own Ingress with a hand-written parser. Only the trait feeds the NetworkPolicy synthesis and is held to the platform’s hostname constraint and the policy’s capability lists. |
kubernetes.CreateIngressClass | networking.k8s.io/v1 IngressClass (cluster-scoped) | kind | ingressclass | strict decode of IngressClassSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties, the default-class annotation (ingressclass.kubernetes.io/is-default-class) included. controller must be written. No environment policy applies. |
kubernetes.CreateJob | batch/v1 Job | kind | job | hand-written parser | - |
kubernetes.CreateLease | coordination.k8s.io/v1 Lease | not authorable | - | - | Written at run time by its holder: a leader-election client, or the kubelet for its node’s heartbeat. |
kubernetes.CreateLimitRange | v1 LimitRange | kind | limitrange | strict decode of LimitRangeSpec | - |
kubernetes.CreateListenerSet | gateway.networking.k8s.io/v1 ListenerSet | kind | listenerset | strict decode of ListenerSetSpec | parentRef with its name and listeners, at least one, must be written, and of each listener its name, port and protocol, as on a Gateway’s. No capability is required. No environment policy applies. |
kubernetes.CreateMutatingAdmissionPolicy | admissionregistration.k8s.io/v1 MutatingAdmissionPolicy (cluster-scoped) | held | - | - | It changes objects of any namespace after the build. Its component follows under go-kure/launcher#943. See “Cluster-wide admission and API registration” above. |
kubernetes.CreateMutatingAdmissionPolicyBinding | admissionregistration.k8s.io/v1 MutatingAdmissionPolicyBinding (cluster-scoped) | held | - | - | It puts a MutatingAdmissionPolicy into effect. Its component follows under go-kure/launcher#943. See “Cluster-wide admission and API registration” above. |
kubernetes.CreateMutatingWebhookConfiguration | admissionregistration.k8s.io/v1 MutatingWebhookConfiguration (cluster-scoped) | kind | mutatingwebhookconfiguration | strict decode of the object, less kind, apiVersion and metadata | As the validatingwebhookconfiguration row, and it changes objects of any namespace after the build, so what the build checked need not hold of what is stored. reinvocationPolicy is written as authored. See “Cluster-wide admission and API registration” above. |
kubernetes.CreateNamespace | v1 Namespace (cluster-scoped) | kind | namespace | strict decode of NamespaceSpec | The component name is the Namespace’s name. Its labels and annotations are the labels and annotations properties. |
kubernetes.CreateNetworkPolicy | networking.k8s.io/v1 NetworkPolicy | kind | networkpolicy | strict decode of NetworkPolicySpec | No type under NetworkPolicySpec unmarshals itself except intstr.IntOrString (a port), a scalar with no nested key to drop. The networkpolicy trait builds its own NetworkPolicy with a hand-written parser and scopes it to its component’s pods; the kind selects what the author wrote. The transform’s NetworkPolicy synthesis in pkg/oam emits NetworkPolicies of its own and reads neither. |
kubernetes.CreateNode | v1 Node (cluster-scoped) | not authorable | - | - | Registered by the kubelet. |
kubernetes.CreatePersistentVolume | v1 PersistentVolume (cluster-scoped) | kind | persistentvolume | strict decode of PersistentVolumeSpec | Held to environment policy on every path that produces one. |
kubernetes.CreatePersistentVolumeClaim | v1 PersistentVolumeClaim | kind | persistentvolumeclaim | hand-written parser | The pvc trait builds through the same path. |
kubernetes.CreatePod | v1 Pod | kind | pod | strict decode of PodSpec | Held to environment policy by the check the rendered paths run on a Pod; ephemeralContainers, priority and overhead refused. |
kubernetes.CreatePodDisruptionBudget | policy/v1 PodDisruptionBudget | kind | poddisruptionbudget | strict decode of PodDisruptionBudgetSpec | No top-level field is required; a defective match expression of selector is refused. No environment policy applies. The scaler trait emits one for its workload too (enablePDB), through its own parser. |
kubernetes.CreatePodTemplate | v1 PodTemplate | kind | podtemplate | strict decode of the object’s template (PodTemplateSpec) | Held to environment policy by the check the rendered paths run on a PodTemplate; the pod spec is held to the pod kind’s refusals, and activeDeadlineSeconds is allowed. Stored, not run: no app label, not a trait target. |
kubernetes.CreatePriorityClass | scheduling.k8s.io/v1 PriorityClass (cluster-scoped) | kind | priorityclass | strict decode of the object, less kind, apiVersion and metadata | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. An unauthored value is emitted as 0. No environment policy applies. |
kubernetes.CreateRangeAllocation | v1 RangeAllocation (cluster-scoped) | not authorable | - | - | The API server’s own allocation record. |
kubernetes.CreateReferenceGrant | gateway.networking.k8s.io/v1 ReferenceGrant | kind | referencegrant | strict decode of ReferenceGrantSpec | from and to must be written, and of a source its group, kind and namespace, of a target its group and kind. The grant is emitted in the build namespace, which is the namespace it allows references into. No capability is required. No environment policy applies. |
kubernetes.CreateReplicaSet | apps/v1 ReplicaSet | kind | replicaset | strict decode of ReplicaSetSpec | Held to environment policy by the check the rendered paths run on a ReplicaSet; the pod template is held to the pod kind’s refusals, activeDeadlineSeconds is refused, and the template gains the app label. |
kubernetes.CreateReplicationController | v1 ReplicationController | kind | replicationcontroller | strict decode of ReplicationControllerSpec | Held to environment policy by the check the rendered paths run on a ReplicationController; the pod template is held as the replicaset kind’s is, activeDeadlineSeconds is refused, and the template gains the app label. selector is optional. |
kubernetes.CreateResourceQuota | v1 ResourceQuota | kind | resourcequota | strict decode of ResourceQuotaSpec | - |
kubernetes.CreateRole | rbac.authorization.k8s.io/v1 Role | kind | role | strict decode of the object, less kind, apiVersion and metadata | Ungated: no capability and no environment-policy check restricts what a role grants. A rule’s verbs, apiGroups and resources must be written; nonResourceURLs are refused. The rbac trait emits one too, through its own hand-written parser. |
kubernetes.CreateRoleBinding | rbac.authorization.k8s.io/v1 RoleBinding | kind | rolebinding | strict decode of the object, less kind, apiVersion and metadata | Ungated: no capability and no environment-policy check restricts what a role grants. roleRef with its kind and name must be written, and a subject’s kind and name. The names are the author’s literals. The rbac trait emits one too, through its own hand-written parser. |
kubernetes.CreateRuntimeClass | node.k8s.io/v1 RuntimeClass (cluster-scoped) | kind | runtimeclass | strict decode of the object, less kind, apiVersion and metadata | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. No environment policy applies. |
kubernetes.CreateSecret | v1 Secret | kind | secret | hand-written parser | The secret trait builds through the same path. The helm component’s secretValues synthesizes the trait. Refused under a policy that forbids explicit secrets, on either path. |
kubernetes.CreateService | v1 Service | kind | service | hand-written parser | - |
kubernetes.CreateServiceAccount | v1 ServiceAccount | kind | serviceaccount | hand-written parser | - |
kubernetes.CreateServiceCIDR | networking.k8s.io/v1 ServiceCIDR (cluster-scoped) | kind | servicecidr | strict decode of ServiceCIDRSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. At least one of cidrs must be written. No environment policy applies. |
kubernetes.CreateStatefulSet | apps/v1 StatefulSet | kind | statefulset | hand-written parser | - |
kubernetes.CreateStorageClass | storage.k8s.io/v1 StorageClass (cluster-scoped) | kind | storageclass | strict decode of the object, less kind, apiVersion and metadata | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties, the default-class annotation (storageclass.kubernetes.io/is-default-class) included. No environment policy applies. |
kubernetes.CreateTCPRoute | gateway.networking.k8s.io/v1 TCPRoute | kind | tcproute | strict decode of TCPRouteSpec | No type under TCPRouteSpec unmarshals itself, so the decode reaches every depth. The kind refuses a route without rules, a rule without backendRefs and a Service backend without its port, which the API server refuses too; of a parent and a backend that are authored, the name must be written. A parent and a backend of another namespace are written as authored, as the httproute kind writes them. No capability is required. No environment policy applies. |
kubernetes.CreateTLSRoute | gateway.networking.k8s.io/v1 TLSRoute | kind | tlsroute | strict decode of TLSRouteSpec | As the tcproute row, and the kind refuses a route without hostnames too, which the API requires of a TLSRoute. The host names are not held to the allowed registries, and their form is left to the API server. |
kubernetes.CreateUDPRoute | gateway.networking.k8s.io/v1 UDPRoute | kind | udproute | strict decode of UDPRouteSpec | As the tcproute row: the decode reaches every depth, the kind refuses a route without rules, a rule without backendRefs and a Service backend without its port, and a parent and a backend of another namespace are written as authored. No capability is required. No environment policy applies. |
kubernetes.CreateValidatingAdmissionPolicy | admissionregistration.k8s.io/v1 ValidatingAdmissionPolicy (cluster-scoped) | held | - | - | It can deny writes for the whole cluster, and no consumer has asked for it. Its component follows under go-kure/launcher#943. See “Cluster-wide admission and API registration” above. |
kubernetes.CreateValidatingAdmissionPolicyBinding | admissionregistration.k8s.io/v1 ValidatingAdmissionPolicyBinding (cluster-scoped) | held | - | - | It puts a ValidatingAdmissionPolicy into effect. Its component follows under go-kure/launcher#943. See “Cluster-wide admission and API registration” above. |
kubernetes.CreateValidatingWebhookConfiguration | admissionregistration.k8s.io/v1 ValidatingWebhookConfiguration (cluster-scoped) | kind | validatingwebhookconfiguration | strict decode of the object, less kind, apiVersion and metadata | The object is named after the component unless objectName names it. A webhook’s name, clientConfig, sideEffects and at least one of admissionReviewVersions must be written; clientConfig holds exactly one of url and service, and a service its namespace and name; a rule names at least one operation, API group, API version and resource; a match condition its name and expression; a selector’s match expression is checked as every selector’s is. The API server calls the webhooks on the requests of any namespace their rules match. Neither the URL nor the Service is resolved at build, and a match condition’s CEL expression is compiled by the API server. The form of every other value (a webhook’s name, the enumerations, the timeout’s range) is left to the API server. Only the object kind rules of the environment policy apply. See “Cluster-wide admission and API registration” above. |
kubernetes.CreateVolumeAttachment | storage.k8s.io/v1 VolumeAttachment (cluster-scoped) | not authorable | - | - | Written by the attach/detach controller. |
kubernetes.CreateVolumeAttributesClass | storage.k8s.io/v1 VolumeAttributesClass (cluster-scoped) | kind | volumeattributesclass | strict decode of the object, less kind, apiVersion and metadata | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. driverName and at least one of parameters must be written. No environment policy applies. |
certmanager.CreateCertificate | cert-manager.io/v1 Certificate | kind | certificate | strict decode of CertificateSpec | Its labels and annotations are the labels and annotations properties. secretName and issuerRef with its name must be written. A keystore password in the object is refused under a policy that forbids explicit secrets. No capability is required. The certificate trait builds a Certificate for a workload through the same constructor, from a hand-written parser. |
certmanager.CreateCertificateRequest | cert-manager.io/v1 CertificateRequest | not authorable | - | - | A one-shot request cert-manager creates for a Certificate. |
certmanager.CreateChallenge | acme.cert-manager.io/v1 Challenge | not authorable | - | - | Created by cert-manager’s ACME issuer. |
certmanager.CreateClusterIssuer | cert-manager.io/v1 ClusterIssuer (cluster-scoped) | kind | clusterissuer | strict decode of IssuerSpec | Its labels and annotations are the labels and annotations properties. No top-level field must be written; of an issuer type that is authored, the fields the API requires that the type would write empty. The cpu and memory of an ACME HTTP01 solver’s pod template are held to the policy’s maxima. No capability is required. |
certmanager.CreateIssuer | cert-manager.io/v1 Issuer | kind | issuer | strict decode of IssuerSpec | As clusterissuer, in the build namespace. Its labels and annotations are the labels and annotations properties. |
certmanager.CreateOrder | acme.cert-manager.io/v1 Order | not authorable | - | - | Created by cert-manager’s ACME issuer. |
cilium.CreateCiliumBGPAdvertisement | cilium.io/v2 CiliumBGPAdvertisement (cluster-scoped) | kind | cilium-bgpadvertisement | strict decode of CiliumBGPAdvertisementSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties; a peer configuration selects it by its labels. The CRD’s five rules on an entry’s type are checked. No environment policy applies. |
cilium.CreateCiliumBGPClusterConfig | cilium.io/v2 CiliumBGPClusterConfig (cluster-scoped) | kind | cilium-bgpclusterconfig | strict decode of CiliumBGPClusterConfigSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. A peer’s address is not held to the allowed registries. No environment policy applies. |
cilium.CreateCiliumBGPNodeConfig | cilium.io/v2 CiliumBGPNodeConfig (cluster-scoped) | not authorable | - | - | Generated by the Cilium operator from a CiliumBGPClusterConfig. |
cilium.CreateCiliumBGPNodeConfigOverride | cilium.io/v2 CiliumBGPNodeConfigOverride (cluster-scoped) | kind | cilium-bgpnodeconfigoverride | strict decode of CiliumBGPNodeConfigOverrideSpec | The object overrides the CiliumBGPNodeConfig of the same name: objectName sets it where the component name is not that name. Its labels and annotations are the labels and annotations properties. No environment policy applies. |
cilium.CreateCiliumBGPPeerConfig | cilium.io/v2 CiliumBGPPeerConfig (cluster-scoped) | kind | cilium-bgppeerconfig | strict decode of CiliumBGPPeerConfigSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. The CRD’s rule on timers is checked on its two fields as authored or as the CRD defaults them. authSecretRef names a Secret and holds no secret. No environment policy applies. |
cilium.CreateCiliumCIDRGroup | cilium.io/v2 CiliumCIDRGroup (cluster-scoped) | kind | cilium-cidrgroup | strict decode of CiliumCIDRGroupSpec | The object is named after the component unless objectName names it, and a Cilium network policy refers to it by that name. Its labels and annotations are the labels and annotations properties; a policy selects a group by its labels. No environment policy applies. |
cilium.CreateCiliumClusterwideEnvoyConfig | cilium.io/v2 CiliumClusterwideEnvoyConfig (cluster-scoped) | held | - | - | As the CiliumEnvoyConfig row, which shares its spec type, and cluster-scoped: its resources are raw Envoy configuration no build-time rule can read, and its services forward the traffic of the Services they name to an Envoy listener (go-kure/launcher#790). |
cilium.CreateCiliumClusterwideNetworkPolicy | cilium.io/v2 CiliumClusterwideNetworkPolicy (cluster-scoped) | kind | cilium-clusterwidenetworkpolicy | strict decode of spec and specs into the Cilium Rule | The decode and the unknown-key check of the cilium-networkpolicy kind (builtin.UnknownCiliumKeyPath). A rule selects endpoints or nodes, exactly one of the two. The fields a rule requires are read from the CRD of the linked module. Its labels and annotations are the labels and annotations properties. No environment policy applies. |
cilium.CreateCiliumEgressGatewayPolicy | cilium.io/v2 CiliumEgressGatewayPolicy (cluster-scoped) | kind | cilium-egressgatewaypolicy | strict decode of CiliumEgressGatewayPolicySpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. The CRD’s rule on egressIP is a value rule and the API server’s. No address or CIDR is held to the allowed registries. No environment policy applies. |
cilium.CreateCiliumEndpoint | cilium.io/v2 CiliumEndpoint | not authorable | - | - | Written by the Cilium agent. |
cilium.CreateCiliumEnvoyConfig | cilium.io/v2 CiliumEnvoyConfig | held | - | - | Its resources are raw Envoy configuration, in the API’s words “Envoy xDS resources” (listeners, routes, clusters, endpoints and TLS secrets), each an opaque Any the CRD keeps unpruned, and its services forward the traffic of the Services they name to an Envoy listener. No build-time rule can read that configuration: the kind would be a way round every rule a policy holds a route, a network policy or a Secret to (go-kure/launcher#790). |
cilium.CreateCiliumIdentity | cilium.io/v2 CiliumIdentity (cluster-scoped) | not authorable | - | - | Written by Cilium when it allocates an identity. |
cilium.CreateCiliumLoadBalancerIPPool | cilium.io/v2 CiliumLoadBalancerIPPool (cluster-scoped) | kind | cilium-loadbalancerippool | strict decode of CiliumLoadBalancerIPPoolSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. An authored disabled: false is the API’s default and is not written. No environment policy applies. |
cilium.CreateCiliumLocalRedirectPolicy | cilium.io/v2 CiliumLocalRedirectPolicy | kind | cilium-localredirectpolicy | strict decode of CiliumLocalRedirectPolicySpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. The CRD’s choice of one frontend matcher is checked; its three rules against a change are the API server’s. No environment policy applies. |
cilium.CreateCiliumNetworkPolicy | cilium.io/v2 CiliumNetworkPolicy | kind | cilium-networkpolicy | strict decode of spec and specs into the Cilium Rule | The endpoint selector, the ICMP field and the rule label unmarshal themselves and drop an unknown key; the kind refuses one by its path at every position that holds any of them (builtin.UnknownCiliumKeyPath), and a test holds the list of such types to the Cilium API. The cilium-networkpolicy trait builds its own CiliumNetworkPolicy from one rule’s endpointSelector, ingress and egress, with the same decode, the same check and the kind’s required list cut to those three fields. |
cilium.CreateCiliumNode | cilium.io/v2 CiliumNode (cluster-scoped) | not authorable | - | - | Written by the Cilium agent for its node. |
cilium.CreateCiliumNodeConfig | cilium.io/v2 CiliumNodeConfig | kind | cilium-nodeconfig | strict decode of CiliumNodeConfigSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. The keys and the values of defaults are not checked. No environment policy applies. |
cnpg.CreateBackup | postgresql.cnpg.io/v1 Backup | kind | cnpg-backup | strict decode of BackupSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. A one-shot request: the API refuses every change of its spec, and applied again it runs nothing. No environment policy applies. |
cnpg.CreateCluster | postgresql.cnpg.io/v1 Cluster | kind | cnpg-cluster | strict decode of ClusterSpec | postgresql lowers onto it. |
cnpg.CreateClusterImageCatalog | postgresql.cnpg.io/v1 ClusterImageCatalog (cluster-scoped) | kind | cnpg-clusterimagecatalog | strict decode of ImageCatalogSpec | As cnpg-imagecatalog, with no namespace. Its labels and annotations are the labels and annotations properties. |
cnpg.CreateDatabase | postgresql.cnpg.io/v1 Database | kind | cnpg-database | strict decode of DatabaseSpec | postgresql lowers onto it. |
cnpg.CreateDatabaseRole | postgresql.cnpg.io/v1 DatabaseRole | kind | cnpg-databaserole | strict decode of DatabaseRoleSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. The CRD’s eight rules on a new object are checked; its two against a change are the API server’s. passwordSecret names a Secret and holds no secret. No environment policy applies. |
cnpg.CreateFailoverQuorum | postgresql.cnpg.io/v1 FailoverQuorum | not authorable | - | - | Written by the CloudNativePG operator. |
cnpg.CreateImageCatalog | postgresql.cnpg.io/v1 ImageCatalog | kind | cnpg-imagecatalog | strict decode of ImageCatalogSpec | The object is named after the component unless objectName names it, and a Cluster refers to it by that name. Its labels and annotations are the labels and annotations properties. The CRD’s two rules (one image per major version, one component image per key) are checked. Every image it names is held to the policy’s allowed registries. |
cnpg.CreateObjectStore | barmancloud.cnpg.io/v1 ObjectStore | kind | cnpg-objectstore | strict decode of ObjectStoreSpec | postgresql lowers onto it. |
cnpg.CreatePooler | postgresql.cnpg.io/v1 Pooler | kind | cnpg-pooler | strict decode of PoolerSpec | postgresql lowers onto it. |
cnpg.CreatePublication | postgresql.cnpg.io/v1 Publication | kind | cnpg-publication | strict decode of PublicationSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. The CRD’s three rules on a target are checked; its four against a change are the API server’s. No environment policy applies. |
cnpg.CreateScheduledBackup | postgresql.cnpg.io/v1 ScheduledBackup | kind | cnpg-scheduledbackup | strict decode of ScheduledBackupSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. The schedule is not parsed. No environment policy applies. |
cnpg.CreateSubscription | postgresql.cnpg.io/v1 Subscription | kind | cnpg-subscription | strict decode of SubscriptionSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. The CRD’s three rules refuse a change and are the API server’s. No environment policy applies. |
externalsecrets.CreateClusterExternalSecret | external-secrets.io/v1 ClusterExternalSecret (cluster-scoped) | kind | clusterexternalsecret | strict decode of ClusterExternalSecretSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties; those of the ExternalSecrets it creates are externalSecretMetadata. externalSecretSpec must be written, with what an externalsecret requires. externalSecretSpec.target.manifest is held to the environment policy as on an externalsecret. |
externalsecrets.CreateClusterSecretStore | external-secrets.io/v1 ClusterSecretStore (cluster-scoped) | kind | clustersecretstore | strict decode of SecretStoreSpec | The object is named after the component unless objectName names it. Its labels and annotations are the labels and annotations properties. provider must be written with exactly one provider; of that provider, the fields the API requires that the type would write empty, and the four it would default. A credential written as a value, and the data of the fake provider, are refused under a policy that forbids explicit secrets. No capability is required. |
externalsecrets.CreateExternalSecret | external-secrets.io/v1 ExternalSecret | kind | externalsecret | strict decode of ExternalSecretSpec | Its labels and annotations are the labels and annotations properties. No top-level field must be written. A generator named as the source of one key of data is refused. A target.manifest whose apiVersion is no API version is refused. A target.manifest of a kind the environment policy checks (a workload, a claim, a PersistentVolume, a HorizontalPodAutoscaler) is refused, and a core Secret there under a policy that forbids explicit secrets. No capability is required. The external-secret trait builds an ExternalSecret for a workload through the same constructor, from a hand-written parser. |
externalsecrets.CreateSecretStore | external-secrets.io/v1 SecretStore | kind | secretstore | strict decode of SecretStoreSpec | As clustersecretstore, in the build namespace. Its labels and annotations are the labels and annotations properties. |
fluxcd.CreateAlert | notification.toolkit.fluxcd.io/v1beta3 Alert | kind | fluxcd-alert | strict decode of AlertSpec | providerRef with its name and eventSources must be written, and of a source its kind and name: the fields the linked Go source marks required, not held to a CRD. A source’s namespace is written as authored. It lands in the Flux namespace when one is set. No environment policy applies. The type name carries a prefix: an alert, in this package, also reads as an alerting rule. |
fluxcd.CreateArtifactGenerator | source.extensions.fluxcd.io/v1beta1 ArtifactGenerator | kind | artifactgenerator | strict decode of ArtifactGeneratorSpec | sources and artifacts must be written; of a source its alias, kind and name, of an artifact its name and copy, of a copy its from and to: the fields the linked Go source marks required, not held to a CRD. A source’s namespace is written as authored. The API’s expression rule, which holds an artifact’s name to an object name where no pathPattern is set, is not checked. It lands in the Flux namespace when one is set. No environment policy applies. |
fluxcd.CreateBucket | source.toolkit.fluxcd.io/v1 Bucket | kind | bucket | strict decode of BucketSpec | - |
fluxcd.CreateExternalArtifact | source.toolkit.fluxcd.io/v1 ExternalArtifact | not authorable | - | - | Written by the controller that produces the artifact. |
fluxcd.CreateFluxInstance | fluxcd.controlplane.io/v1 FluxInstance | held | - | - | A FluxInstance is the installation of Flux itself, not an application’s object: its spec lists “the controllers to install”, and the API accepts one name for it, flux. A component type would let an application author the cluster’s Flux (go-kure/launcher#790). |
fluxcd.CreateFluxReport | fluxcd.controlplane.io/v1 FluxReport | not authorable | - | - | Written by the Flux operator. |
fluxcd.CreateGitRepository | source.toolkit.fluxcd.io/v1 GitRepository | kind | gitrepository | strict decode of GitRepositorySpec | - |
fluxcd.CreateHelmChart | source.toolkit.fluxcd.io/v1 HelmChart | kind | helmchart | strict decode of HelmChartSpec | - |
fluxcd.CreateHelmRelease | helm.toolkit.fluxcd.io/v2 HelmRelease | kind | helmrelease | strict decode of HelmReleaseSpec | helm lowers onto it. |
fluxcd.CreateHelmRepository | source.toolkit.fluxcd.io/v1 HelmRepository | kind | helmrepository | strict decode of HelmRepositorySpec | - |
fluxcd.CreateImagePolicy | image.toolkit.fluxcd.io/v1 ImagePolicy | kind | imagepolicy | strict decode of ImagePolicySpec | imageRepositoryRef with its name and policy must be written, and of a semver policy its range: the fields the linked Go source marks required, not held to a CRD. The repository’s namespace is written as authored. interval is held to the pattern of a Flux duration; the API’s two expression rules, which tie it to digestReflectionPolicy: Always, are not checked. It lands in the Flux namespace when one is set. No environment policy applies. |
fluxcd.CreateImageRepository | image.toolkit.fluxcd.io/v1 ImageRepository | kind | imagerepository | strict decode of ImageRepositorySpec | image and interval must be written, and of an authored secretRef, proxySecretRef or certSecretRef its name, of an authored accessFrom its namespaceSelectors: the fields the linked Go source marks required, not held to a CRD. interval and timeout are each held to the pattern of their field; timeout takes no h, and one of an hour or more is written in minutes. It lands in the Flux namespace when one is set. The registry of image is held to the allowed registries of the environment policy; no tag rule applies. |
fluxcd.CreateImageUpdateAutomation | image.toolkit.fluxcd.io/v1 ImageUpdateAutomation | kind | imageupdateautomation | strict decode of ImageUpdateAutomationSpec | sourceRef with its kind and name and interval must be written, and of an authored git its commit with the author’s email: the fields the linked Go source marks required, not held to a CRD. sourceRef.kind is defaulted by the API and required here, since the type writes it empty. The source’s namespace is written as authored, and so is where the automation pushes. interval is held to the pattern of a Flux duration. It lands in the Flux namespace when one is set. No environment policy applies. |
fluxcd.CreateKustomization | kustomize.toolkit.fluxcd.io/v1 Kustomization | kind | fluxcd-kustomization | strict decode of KustomizationSpec | oci lowers onto it. targetNamespace is never defaulted. |
fluxcd.CreateOCIRepository | source.toolkit.fluxcd.io/v1 OCIRepository | kind | ocirepository | strict decode of OCIRepositorySpec | oci lowers onto it. |
fluxcd.CreateProvider | notification.toolkit.fluxcd.io/v1beta3 Provider | kind | fluxcd-provider | strict decode of ProviderSpec | type must be written, and of an authored secretRef, proxySecretRef or certSecretRef its name: the fields the linked Go source marks required, not held to a CRD. interval and timeout are each held to the pattern of their field; timeout takes no h, and one of an hour or more is written in minutes. It lands in the Flux namespace when one is set. A user or a password in address or proxy is refused. Under a policy that forbids explicit secrets an address that is the credential is refused too: that of discord, generic, generic-hmac, googlechat, lark, msteams, rocket and slack, and an azureeventhub address that holds SharedAccessKey. |
fluxcd.CreateReceiver | notification.toolkit.fluxcd.io/v1 Receiver | kind | fluxcd-receiver | strict decode of ReceiverSpec | type and resources must be written, of each resource its kind and name, of an authored secretRef its name, and of an OIDC provider its issuerURL and validations with the fields of each validation and variable: the fields the linked Go source marks required, not held to a CRD. interval is held to the pattern of a Flux duration. It lands in the Flux namespace when one is set. No environment policy applies. |
fluxcd.CreateResourceSet | fluxcd.controlplane.io/v1 ResourceSet | held | - | - | Its resourcesTemplate is, in the API’s words, “a Go template that generates the list of Kubernetes resources to reconcile”. The operator renders it on the cluster, so no build sees the objects and none can be held to a rule: the kind would be a way round every rule a policy holds a workload or a Secret to (go-kure/launcher#790). |
fluxcd.CreateResourceSetInputProvider | fluxcd.controlplane.io/v1 ResourceSetInputProvider | kind | resourcesetinputprovider | strict decode of ResourceSetInputProviderSpec | type must be written, of an authored secretRef or certSecretRef its name and of a schedule its cron: the fields the linked Go source marks required. A schedule’s window is held to the pattern of a Flux duration by its authored text; filter.limit cannot be authored as 0, nor a schedule’s timeZone as empty. It lands in the Flux namespace when one is set. A user or a password in url is refused, and the host of url is held to the allowed registries of the environment policy. |
metallb.CreateBFDProfile | metallb.io/v1beta1 BFDProfile | kind | metallb-bfdprofile | strict decode of BFDProfileSpec | The object is named after the component unless objectName names it, and a MetalLB BGPPeer refers to it by that name. No field is required. Every field is a pointer, so an authored 0 or false is written; the bounds of the numbers are not checked. It is written in the build namespace; MetalLB reads its objects in the one namespace it is configured to watch, by default the one it runs in. No capability is required. No environment policy applies. |
metallb.CreateBGPAdvertisement | metallb.io/v1beta1 BGPAdvertisement | kind | metallb-bgpadvertisement | strict decode of BGPAdvertisementSpec | The object is named after the component unless objectName names it. No field is required: one that authors nothing is the widest advertisement, of every pool, to every peer, for every Service, with no node excluded. A service selector beside an aggregation length other than the API’s default is refused, as the CRD’s expression rule refuses it. It is written in the build namespace; MetalLB reads its objects in the one namespace it is configured to watch, by default the one it runs in. The communities and the pool and peer names are not read. No capability is required. No environment policy applies. |
metallb.CreateBGPPeer | metallb.io/v1beta2 BGPPeer | kind | metallb-bgppeer | strict decode of BGPPeerSpec | The object is named after the component unless objectName names it, and a MetalLB BGPAdvertisement refers to it by that name. myASN must be written. A peerPort: 0 is refused: the type would leave it out and the API server would fill 179. A connectTime is held to the CRD’s two expression rules; the CRD’s rule against a change of enableGracefulRestart is the API server’s. A password is refused under a policy that forbids explicit secrets; passwordSecret names a Secret and holds no secret, and is written as {} where it is not authored. It is written in the build namespace; MetalLB reads its objects in the one namespace it is configured to watch, by default the one it runs in. The addresses and the profile’s name are not read. No capability is required. |
metallb.CreateCommunity | metallb.io/v1beta1 Community | kind | metallb-community | strict decode of CommunitySpec | The object is named after the component unless objectName names it; a MetalLB BGPAdvertisement refers to an alias by the alias’s own name, not the object’s. No field is required, of the spec or of an alias. The form of a value is not read, and neither is a name defined twice. It is written in the build namespace; MetalLB reads its objects in the one namespace it is configured to watch, by default the one it runs in. No capability is required. No environment policy applies. |
metallb.CreateConfigurationState | metallb.io/v1beta1 ConfigurationState | not authorable | - | - | Status MetalLB writes. |
metallb.CreateIPAddressPool | metallb.io/v1beta1 IPAddressPool | kind | metallb-ipaddresspool | strict decode of IPAddressPoolSpec | The object is named after the component unless objectName names it, and a MetalLB advertisement refers to it by that name or selects it by its labels, the labels property. It is written in the build namespace; MetalLB reads its objects in the one namespace it is configured to watch, by default the one it runs in. The addresses are not read. No capability is required. No environment policy applies. |
metallb.CreateL2Advertisement | metallb.io/v1beta1 L2Advertisement | kind | metallb-l2advertisement | strict decode of L2AdvertisementSpec | The object is named after the component unless objectName names it. No field is required: one that authors nothing is the widest advertisement, of every pool, on every interface, for every Service, with no node excluded. It is written in the build namespace; MetalLB reads its objects in the one namespace it is configured to watch, by default the one it runs in. The pool and interface names are not read. No capability is required. No environment policy applies. |
metallb.CreateServiceBGPStatus | metallb.io/v1beta1 ServiceBGPStatus | not authorable | - | - | Status MetalLB writes. |
metallb.CreateServiceL2Status | metallb.io/v1beta1 ServiceL2Status | not authorable | - | - | Status MetalLB writes. |
prometheus.CreateAlertmanager | monitoring.coreos.com/v1 Alertmanager | kind | alertmanager | strict decode of AlertmanagerSpec | Held to the environment policy as a workload is, for what the spec says of the pods the operator runs: image, replicas, resources, the storage of the claim the operator makes, and containers, initContainers, volumes, securityContext and hostNetwork as a pod’s; the replica count and memory request the operator fills where they are unset are held, not written. baseImage, tag and sha are refused when not empty, and so are a retention and a cluster duration of 0 or less. An image the operator chooses, for alertmanager or one of its two reloaders, is refused under a policy with allowed registries. podMetadata is read for reserved keys and takes no label: the operator’s pods carry the component label only where the author writes it there. No capability is required. |
prometheus.CreatePodMonitor | monitoring.coreos.com/v1 PodMonitor | kind | podmonitor | strict decode of PodMonitorSpec | selector must be written, and the three required fields of an endpoint’s oauth2. No environment policy applies, and no capability is required. |
prometheus.CreateProbe | monitoring.coreos.com/v1 Probe | kind | prometheus-probe | strict decode of ProbeSpec | prober.url must be written, and the three required fields of an oauth2. No environment policy applies, and no capability is required. The type name carries a prefix: a probe, in this package, is a container’s. |
prometheus.CreatePrometheus | monitoring.coreos.com/v1 Prometheus | kind | prometheus | strict decode of PrometheusSpec | Held to the environment policy as a workload is, for what the spec says of the pods the operator runs: image and thanos.image, replicas times shards, resources and thanos.resources, the storage a claim template requests, and containers, initContainers, volumes, securityContext and hostNetwork as a pod’s; and the deprecated bearerToken of remoteWrite[], remoteRead[] and apiserverConfig under a policy that forbids explicit secrets. A credential in a header or a URL is not read. baseImage, tag and sha are refused when not empty, and the same three of thanos whenever set. The pod count the operator runs, an unset replica or shard count read as 1, is held, not written. An image the operator chooses, for prometheus, its two reloaders or, where thanos is set, thanos-sidecar, is refused under a policy with allowed registries. podMetadata is read for reserved keys and takes no label: the operator’s pods carry the component label only where the author writes it there. No capability is required. |
prometheus.CreatePrometheusRule | monitoring.coreos.com/v1 PrometheusRule | kind | prometheusrule | strict decode of PrometheusRuleSpec | A group’s name and a rule’s expr must be written. No environment policy applies, and no capability is required. |
prometheus.CreateServiceMonitor | monitoring.coreos.com/v1 ServiceMonitor | kind | servicemonitor | strict decode of ServiceMonitorSpec | endpoints and selector must be written, and the three required fields of an endpoint’s oauth2. No environment policy applies, and no capability is required. |
prometheus.CreateThanosRuler | monitoring.coreos.com/v1 ThanosRuler | kind | thanosruler | strict decode of ThanosRulerSpec; its labels is the externalLabels property | Held to the environment policy as a workload is, for what the spec says of the pods the operator runs: image, replicas, resources, the storage of the claim the operator makes, and containers, initContainers, volumes and securityContext as a pod’s; the replica count and memory request the operator fills where they are unset are held, not written; and the deprecated remoteWrite[].bearerToken under a policy that forbids explicit secrets. A credential in a header or a URL is not read. An image the operator chooses, for thanos-ruler or its config-reloader, is refused under a policy with allowed registries. podMetadata is read for reserved keys and takes no label: the operator’s pods carry the component label only where the author writes it there. No capability is required. |
volsync.CreateReplicationDestination | volsync.backube/v1alpha1 ReplicationDestination | kind | replicationdestination | strict decode of ReplicationDestinationSpec | As replicationsource, without a Syncthing mover. Its labels and annotations are the labels and annotations properties. |
volsync.CreateReplicationSource | volsync.backube/v1alpha1 ReplicationSource | kind | replicationsource | strict decode of ReplicationSourceSpec | No top-level field must be written; of a volume mounted into a mover that is authored, its mountPath and volumeSource, of a Syncthing peer its address, ID and introducer, and of a match expression in a label selector of a mover’s moverAffinity its key and operator. An authored capacity is held to the policy’s storage maximum, a mover’s cpu and memory to its maxima, and a mover’s hostProcess switch is refused unless privileged workloads are allowed. An rsync mover is held to the policy’s container capabilities for the seven the linked operator version adds to its container. No capability is required. The volsync trait builds a ReplicationSource for a workload’s claim through the same constructor, from a hand-written parser. Its labels and annotations are the labels and annotations properties. |
Adding a kind component: where its lines go
A change that adds a kind component adds one entry to each of the lists below. Such changes
are written side by side, and two of them merge without a conflict only where their new
lines land between different neighbours. So a new entry goes at the position of its
component type, in the order sort.Strings gives (cilium-nodeconfig before
clusterissuer, pod before poddisruptionbudget), and never at the end of the list.
| List | Where | Held in order by |
|---|---|---|
ComponentHandlers | pkg/oam/builtin/registry/registry.go | TestKindLists_InOrder (pkg/cmd/kurel) |
validComponentTypes | pkg/oam/validate.go | TestKindLists_InOrder |
wantHandlers, componentLabelFixtures | the tests of pkg/cmd/kurel | TestKindLists_InOrder |
coreKindSchemas, apiSetKinds | the tests of this package | TestKindLists_InOrder; TestKindLists_Complete fails on a registered type with no row and no reason in kindListExceptions |
parityKinds, for a kind excepted there because it reads its own properties (ownProperties) | hand_parsed_parity_internal_test.go | not in order; TestKindLists_OwnPropertiesHaveParityRows (pkg/cmd/kurel) fails on a type with that reason and no row, and on a row of a type without it |
policyFreeKinds and the two maps of its tests, for a kind built on policyFreeKind | the tests of this package | TestKindLists_InOrder |
requiredWrittenKinds, for a kind whose API is a CRD a linked module ships or whose markers are read; monitoringKinds, for a kind of the Prometheus operator’s API | the tests of this package | TestKindLists_InOrder. testdata/required-written-not-refused.txt is written in the order of requiredWrittenKinds, so a new kind’s lines land at its position when it is regenerated |
imageFieldTypes, for a spec type a kind decodes that names an image | image_fields_internal_test.go | TestKindLists_InOrder, in the order of the entries’ name |
kindTraitPairs, for a kind a trait also generates | the tests of pkg/cmd/kurel | TestKindLists_InOrder, in the order of the entries’ typ |
policyFreeTypes, for a kind built on policyFreeKind whose type publishes its field comments | kind_policy_free_internal_test.go | no test: its rows name the kind’s value, not its type, and a new row goes at the end. A kind with no row is not held by TestPolicyFreeKinds_NoDefaultedZeros |
| “Component types” | this file | TestKindLists_InOrder |
| “Component type allowlist” | pkg/oam/README.md | TestKindLists_InOrder; TestKindLists_Complete fails on a type of validComponentTypes with no row and on a row of no type of it |
| Table 4.2 | docs/oam/design-kurel-package.md | TestKindLists_InOrder |
The table of kind components under “kurel build” | pkg/cmd/kurel/README.md | TestKindLists_InOrder |
| “Per-type highlights” | this file | no test, and not yet in order: a new entry goes before the first entry whose head names a type that sorts after its own |
| The “Shipped” entries of §6.2 that ship kinds | docs/delivery-scope.md | no test: the same position, before the entries that are not “Shipped” |
In the Go lists an entry names its component type by a string literal, not by a constant (a
struct written with field names, in its component field or the one kindListKeyFields in
pkg/cmd/kurel names for its list): TestKindLists_InOrder reads the source file, and stops
on an entry whose type it cannot read.
What belongs to one kind goes in that kind’s own files, not at the end of a shared one: the
handler, its ContractMetadata and ComponentObject methods (not contract.go,
object_name.go), its tests, and the helpers only its entries in a shared list use, which go
in its own test file rather than beside the list.
No sentence names every kind. A sentence that needs the set points at the “Kind inventory”,
which its tests hold complete. One table names every component type, one row each: the
“Component type allowlist” of pkg/oam/README.md. A change that adds a type to
validComponentTypes adds its row there, so it changes the README that CI’s documentation
check requires of a change to pkg/oam.
Six places still conflict, and there the change that lands second is rebased on the first before it is published:
policyFreeTypes, where two changes each add a row at the end;- two changes that add kinds at the same position of a list, with no existing entry between
them (two more
cilium-*kinds, say); - a map whose values
gofmtaligns (ComponentHandlers,validComponentTypes,kindListExceptions), when a new type is longer than every type the map holds: every entry is realigned, which conflicts with any other change to that map; - the “Kind inventory”, where a new kind edits its row in place (
missingtokind): two changes that edit neighbouring rows; - the entries of §6.2 of
docs/delivery-scope.mdthat are not “Shipped” (a “Held” entry, “Missing kinds”), which have no order: two changes that each add one at the same place, or that edit the same one; - the import block of a file both changes add an import to.
Common config
env, command, args, initContainers, sidecars and affinity each read
their property with a bare comma-ok type assertion and, when that failed,
returned the zero value with no error. So a property authored with the wrong
container type — a mapping where the schema wants a list, a list or scalar
where it wants an object — built cleanly and emitted nothing for the property,
and nothing in the output said it had been seen. Closed in go-kure/launcher#423
by routing those six through presence-reporting helpers (common.go), which
report presence separately from value, so each of the six now rejects a
mistyped value by name instead of discarding it. (go-kure/launcher#444 later
folded the dedicated optionalObject/optionalObjectList/optionalStringList
wrappers those six originally used into the base parseObjectField/
parseStringField/etc. helpers themselves — callers now use the base helpers
directly, and the null-as-absence behavior applies uniformly rather than only
through the wrappers.) Those helpers were already how several other properties on the
same components are read — updateStrategy and ordinals on statefulset
(statefulset_spec.go), successPolicy and podFailurePolicy on job and
cronjob (common.go) — so the six behaved differently from properties an
author writes beside them in the same document. This covers those six parsers;
it is not a claim that every property in this package is read through the
helpers.
An env entry with no usable name is an error at every position, not only
at the top level. The published schema marks env[].name required, but that
check reached only a top-level env list: an env list nested inside an
initContainers or sidecars entry sat in an open object the schema did not
describe, and parseEnv then skipped a nameless entry, so the container was
emitted with the variable missing and nothing reported. parseEnv now refuses
it itself, whether or not the schema layer reaches that position —
env[0]: name is required for an absent, empty or null name, and
env[0].name: must be a string, got int for a non-string one — so the rule
holds for any caller, including one that hands properties to a handler without
schema validation. Behavior-changing under launcher.gokure.dev/v1alpha1:
a document authoring such an entry inside initContainers/sidecars built
before (with the variable dropped) and is now rejected, and so is a top-level
entry with name: "" — schema validation checks only that name is present
and a string, so the empty name reached parseEnv and was skipped the same
way. A top-level entry with name absent or null was already refused by
schema validation and still is (go-kure/launcher#447).
A required string field given a value of the wrong type is reported as a type
error, not as missing. Fourteen required-string reads used to discard a failed
type assertion and then test for "", so fieldPath: 123 produced
fieldRef: fieldPath is required and sent the author looking for a lost key.
They now read through requiredStringField (or parseStringField directly
where the existing message is kept verbatim), and each field gets its own
messages: fieldRef.fieldPath: must be a string, got int for a wrong type, and
fieldRef: fieldPath is required only for an absent, empty or null value. The
fields are env[].valueFrom.fileKeyRef.volumeName/.path/.key,
valueFrom.fieldRef.fieldPath, valueFrom.resourceFieldRef.resource,
envFrom[].configMapRef.name and .secretRef.name, the name and image of
an initContainers/sidecars entry and the name/mountPath of its
volumeMounts, manifests
scopeOverrides[].apiVersion/.kind, and oci source.url and version.
fileKeyRef, volumeMounts and scopeOverrides used to report their fields
together (“volumeName, path, and key are all required”), so a wrong type on
one looked like any of them might be missing; each field is now named on its
own. What is accepted is unchanged: every one of these values was rejected
before and still is, and only the message changed. For a document run through
schema validation, the schema already refuses a non-string at every one of
these positions, so the parser message is what a caller sees that hands
properties to a handler without schema validation (go-kure/launcher#453).
The main container is named after the component, so a workload component
name must be a DNS-1123 label, not merely a subdomain. A component name is
validated as a DNS-1123 subdomain (pkg/oam/validate.go), which permits dots,
and that name reaches metadata.name unchanged and the app: label through
oam.ComponentLabelValue (the name itself at 63 characters or fewer) — both
accept it. It also becomes the name of the pod’s main container, and a
container name is a DNS-1123 label, which forbids dots and allows at most 63
characters where a subdomain allows 253. batch.worker would therefore build
a workload the API server rejects at admission, naming
spec.template.spec.containers[0].name, a field the author never wrote; an
undotted name longer than 63 characters is refused by the same check. All
seven workload kinds (webservice, worker, deployment, statefulset,
daemonset, cronjob, job) now refuse such a name at generation, in the one
builder they share. The name is refused rather than rewritten to batch-worker:
a derived name would silently rename the container and could collide with an
init container or sidecar of that name. Component types that name no container
after the component (helmrelease, manifests, passthrough, …) keep accepting
a dotted or longer name, so the component-name rule itself is unchanged. job refused
the name from its introduction; the other six gained the check in
go-kure/launcher#407. Nothing that previously produced an applyable manifest is
affected — such a document never did. A kind that also names a Service after
the component (webservice) refuses a dotted or longer name earlier, at
conversion, by the stricter Service-name rule described under each kind in
“Per-type highlights” (go-kure/launcher#546). daemonset and statefulset
named one too until they stopped emitting a Service (go-kure/launcher#690).
cronjob and job refuse a name past the length of their own object earlier
too, at conversion: a CronJob’s name is at most 52 characters, a Job’s at most
63 (see each kind in “Per-type highlights”).
Most workload types (webservice, worker, deployment, statefulset,
daemonset, cronjob, job)
share these fields, projected directly onto real corev1 types (same
structural pattern as ProbeConfig holding *corev1.Probe) rather than a
hand-rolled parallel schema: image (validated — no untagged/latest), env
(value or valueFrom — mutually exclusive, matching corev1.EnvVar’s own
doc comment (“cannot be used if value is not empty”); valueFrom is one of
secretKeyRef, configMapKeyRef (both accept optional, rejecting a
present non-boolean value rather than silently treating it as unset; a key
other than name/key/optional in either is rejected outright too, rather
than being silently ignored), fieldRef
(apiVersion must be v1 if authored — the only field-label conversion
Kubernetes has ever shipped for the downward API; omitting it also defaults to
v1; a present-but-non-string apiVersion (e.g. a bare YAML number) is
rejected rather than silently treated as absent (parseStringField);
fieldPath is validated against the exact set real admission accepts
for an env var fieldRef — metadata.name/metadata.namespace/metadata.uid,
spec.nodeName/spec.serviceAccountName, status.hostIP/status.hostIPs/
status.podIP/status.podIPs — plus the metadata.labels['KEY']/
metadata.annotations['KEY'] subscript forms, each with KEY checked as a
qualified name; a field like status.phase builds but is rejected by
admission, so it is rejected here too; a key other than fieldPath/
apiVersion is rejected outright too, rather than being silently ignored),
resourceFieldRef (resource must be
one of limits.cpu, limits.memory, limits.ephemeral-storage,
requests.cpu, requests.memory, requests.ephemeral-storage, or a
requests.hugepages-<size>/limits.hugepages-<size> selector — the downward
API cannot project an arbitrary extended resource such as
limits.nvidia.com/gpu, unlike a plain resources map below; an authored
divisor must be one of the canonical unit strings admission accepts for that
resource’s family — 1m/1 for cpu, one of 1/1k/1M/1G/1T/1P/1E/
1Ki/1Mi/1Gi/1Ti/1Pi/1Ei for memory/ephemeral-storage/hugepages — a
zero-valued divisor such as "0" is rejected outright rather than silently
treated as absent: Kubernetes’ own zero-value defaulting substitutes 1 for a
zero divisor, so silently accepting one would change the emitted unit without
the author asking for it; a present-but-non-string
divisor (e.g. a bare YAML number) is rejected rather than silently treated as
absent (parseStringField);
containerName, if authored,
must be a syntactically valid container name (ValidateDNS1123Label) —
note: whether it actually names a container present in the generated pod
is deliberately not checked, since that needs the full sibling
container/initContainer/sidecar name set, which isn’t available at this
single-env-var parsing depth; real admission doesn’t check it either (only
the downward API volume form of resourceFieldRef requires
containerName at all), so an unresolvable target only surfaces later, as a
kubelet-time CreateContainerConfigError; a key other than
resource/containerName/divisor is rejected outright rather than
silently ignored, since a typo such as divisorr would otherwise leave the
divisor unset — Kubernetes treats a zero divisor as its default of 1,
changing the emitted unit),
fileKeyRef (volumeName/path/key required, optional accepted (same
non-boolean rejection as above); corev1’s EnvFiles feature; volumeName
must be a valid DNS-1123 label and key a valid (relaxed) env var name,
matching real admission’s own validateFileKeySelector; path must be
relative and must not contain a .. backstep component, per this repo’s own
path-safety convention; once the component’s volumes are parsed,
volumeName must name one of them of type emptyDir — a fileKeyRef in
the main container, an init container or a sidecar naming an undeclared
volume (a statefulset claim template included) or a non-emptyDir one is
refused, as real admission’s validateFileKeyRefVolumes refuses it (no
trait the build evaluates adds an emptyDir volume; a patch a consumer
applies at delivery is outside the build’s view, so a volume only a patch supplies is not
seen — declare the emptyDir volume on the component itself; see
checkFileKeyRefVolumes in common.go); a
key other than volumeName/path/key/optional is rejected outright too,
rather than being silently ignored) —
mutually exclusive among themselves too), envFrom (an authored non-array
value, e.g. a single ConfigMap/Secret object instead of a list of them, is
rejected rather than silently treated as absent; bulk-import a ConfigMap’s
or Secret’s keys, with prefix — any printable ASCII character except =,
matching corev1.EnvFromSource.Prefix’s own field doc comment; only the final
prefix+key concatenation need be a valid env var name, not the prefix alone;
a present-but-non-string prefix (e.g. a bare YAML number) is rejected
rather than silently omitted while still emitting the rest of the source —
an unprefixed import can collide with existing names and leaves the names
the application expects unset;
configMapRef.name/secretRef.name must each be a valid DNS-1123 subdomain,
matching how every Kubernetes object name is validated; both configMapRef
and secretRef also accept optional, with the same non-boolean rejection
as env’s secretKeyRef/configMapKeyRef above; a key other than
name/optional inside either nested ref (e.g. a misspelled optoinal) is
rejected outright too, rather than silently leaving the ref at its required
default; a present-but-malformed
configMapRef/secretRef (e.g. a scalar instead of an object) is rejected
outright rather than silently treated as absent — the latter would let the
other, well-formed ref alone satisfy the “exactly one” check below and
quietly discard the malformed one instead of reporting it); a key other
than prefix/configMapRef/secretRef (e.g. a misspelled prefx) is
rejected outright too, rather than being silently ignored while the rest of
the entry still builds — on prefix specifically, that previously emitted
an unprefixed import instead of the intended one),
resources — a corev1.ResourceRequirements projection read with
parseObjectField, as are its requests and limits: a present value of the
wrong type (resources: "big", requests: 3) is rejected by name
(resources: must be an object, got string) by each of the eight kinds that accept resources (webservice, worker,
deployment, statefulset, daemonset, cronjob, job, postgresql) and by each
initContainers/sidecars entry, and a null reads as absence. Until
go-kure/launcher#405 all three were read with a bare comma-ok assertion, so a
handler called directly — without the schema validation an authored document
gets first, which already refused these shapes — dropped the value and emitted
a container with no requests or limits. requests/limits
accept cpu/memory (defaults 100m/128Mi — subject to the environment
policy’s MaxCPU()/MaxMemory() maxima the same as an authored value; see
“Policy defaults & enforcement ordering” below) plus any other well-formed
resource name (e.g. ephemeral-storage, nvidia.com/gpu) in the same map,
each value authored as either a quantity string ("500m", "2Gi") or a bare
YAML/JSON number (1, 0.5) — both are valid resource.Quantity input
(Quantity.UnmarshalJSON parses a bare numeric literal the same way it parses
a quoted one), and both forms are also accepted by the published property
schema itself: cpu/memory are declared as the Types: [string, number]
union (go-kure/launcher#383), so a value of any other type is rejected by
property validation before the parser sees it — parsed as resource.Quantity and
round-tripped unmodified. A resource name is validated the same way real
admission validates corev1.Container.Resources (mirrors
ValidateContainerResourceName): an unqualified name (no /) must be
cpu/memory/ephemeral-storage or a hugepages-<size> name — an arbitrary
unqualified token such as foo builds but is reserved for Kubernetes’ own
native resources and is rejected here too; a qualified name (has /) is
accepted as an extended resource (e.g. nvidia.com/gpu) unless it either
contains kubernetes.io/ (which claims to be a native resource, not an
extended one, mirroring IsNativeResource) or is requests.-prefixed (which
collides with the ResourceQuota requests.<name> alias form) — the two
conditions are checked independently, so e.g. kubernetes.io/foo is rejected
even though it is not requests.-prefixed.
Every quantity must be non-negative; cpu/memory/storage/
ephemeral-storage may be fractional, but any other (extended) resource name
must be a whole number, matching Kubernetes’ own extended-resource
constraint. A hugepages-<size> quantity must additionally be an integer
multiple of that page size (e.g. hugepages-2Mi: 3Mi is a whole number of
bytes but not a whole number of 2Mi pages, and is rejected; hugepages-2Mi: 4Mi is accepted), matching IsHugePageResourceValueDivisible. A hugepages
or extended resource cannot be overcommitted (mirrors
validateResourceRequirements’s IsOvercommitAllowed check): if requests
sets one, limits must set the identical value for that same name — a
request with no matching limit is rejected outright (nothing defaults a
missing limit from a request, unlike cpu/memory), and a request/limit pair
that merely differs is rejected too. A limits-only entry with no matching
requests entry is deliberately not rejected here: the real apiserver’s
defaulter copies limits into requests before validation runs, so that
shape is admission-valid. cpu/memory/ephemeral-storage stay
overcommittable — a lower request than limit is fine, and either may be set
without the other — but when both are present the request still must not
exceed the limit, matching validateResourceRequirements’s
request-vs-limit comparison for every resource name, not just the
non-overcommitable set. Admission’s “HugePages require cpu or memory” rule
(a block naming a hugepages-<size> resource must also name cpu or memory,
on either side) is not checked here, because a container’s final resources
always carry the cpu/memory requests buildResourceRequirements defaults in;
it is checked where a block reaches the cluster without those defaults —
podResources at parse time, and the postgresql Cluster’s resources at
generation time. No policy
default/max hook exists for names other than cpu/memory
today; the container-level claims list (Dynamic Resource Allocation) is
deliberately not covered — it only references pod-level claims by name, and
the pod-level resourceClaims property that declares them is now accepted
(see Pod-level properties below, go-kure/launcher#342), so what remains
missing is the container-side reference list alone, tracked with the rest of
DRA support, see parseResources’s doc comment. An authored claims is
refused by name, whatever its value, on the main container and on an
initContainers or sidecars entry — resources.claims: not read by this component — … — and no longer dropped for a caller that skips the document
check; see
Upstream fields a hand-parsed kind does not read),
command/args (must be an
array, and each element must be a string — both are rejected outright rather
than silently discarded. Until go-kure/launcher#423 they were mishandled twice
over: a mistyped command: /bin/sh -c true fell through their comma-ok guard
and the container built with no command at all, and command: [ls, 3] emitted
["ls"] and said nothing about the 3. Closing it changed their signature to
return an error, which is why the fix touches all 9 call sites — the seven
workload kinds’ own main containers, plus initContainers and sidecars in
common.go), probes
(rejected outright if authored with a non-object value, e.g. probes: true,
and likewise for each of its own readiness/liveness/startup keys, e.g.
probes: {liveness: true} — same two-level presence-then-type-check shape as
lifecycle below, instead of silently discarding the authored health check;
a key other than readiness/liveness/startup (e.g. a misspelled
probes: {live: {...}}) is rejected outright too, rather than matching none
of the three recognized keys and silently producing no probe at all; a key
other than the ten recognized fields inside a single probe object — the four
handlers below plus the six timing fields further down — (e.g. a misspelled
failureTreshold) is rejected outright too, instead of silently falling
back to Kubernetes’ own default for the field the typo was meant to
override;
httpGet/tcpSocket/exec/grpc — exactly one handler may be authored; a
present-but-non-object value for any of the four (e.g. httpGet: "invalid")
is rejected outright, even when paired with a well-formed sibling handler,
rather than silently discarded while the sibling wins; an authored probe
object with none of the four handlers present (e.g. probes: {liveness: {periodSeconds: 10}}) is rejected too, matching real admission
(validateHandler’s numHandlers == 0 check) instead of silently discarding
the whole authored probe; exec.command must be
a non-empty array of strings — a present-but-wrong-type command (e.g. a
bare string instead of an array) or an array containing a non-string element
is rejected the same way, instead of silently producing no probe at all,
mirroring lifecycle.{postStart,preStop}.exec.command below; a key other
than command inside a probe exec object (e.g. a misspelled commnad) is
rejected outright too, instead of silently ignored while the intended
command never overrides the default; a string port is a named container port, not
an arbitrary label, and is validated against validation.IsValidPortName
just as real admission’s ValidatePortNumOrName does: lowercase
[-a-z0-9] only, at least one letter, no leading/trailing/adjacent hyphen,
max 15 characters — a purely numeric string like "8080" is rejected, since
it has no letter; beyond syntax, a named port is rejected outright on a
component kind whose main container never declares any port for the kubelet
to resolve the name against — worker and cronjob unconditionally (neither
exposes a port property at all), daemonset/statefulset only when their
own optional port was not set on that component instance (their main
container is named "http"/"tcp" respectively, but only when port > 0),
and webservice never (its port always defaults to 80, so the main
container is always named "http") — a numeric port is unaffected either way,
since it dials the kubelet directly rather than resolving a declared name.
(Those rules predate ports, and the port they describe on daemonset and
statefulset is gone (go-kure/launcher#690). Today deployment, daemonset,
statefulset, job and cronjob resolve a named port against the main
container’s ports list, refusing a name that list does not declare and any
named port when it is empty; see “Main container ports”.)
Where a named port is allowed at all, it is further checked against the
exact names the main container actually declares — "http" for
webservice, the authored ports names on the kinds above — not merely accepted as
any syntactically valid name: a syntactically valid but different name (e.g.
httpGet.port: metrics on a webservice, whose only declared container
port is "http") builds successfully but is exactly as unresolvable by the
kubelet at runtime as a named port on a portless component, so it is
rejected the same way; grpc.service, if authored, must be a string — a
present-but-non-string value (e.g. service: 123) is rejected rather than
silently treated as absent (which would check the overall server instead of
the intended named service) — and must be no more than 63
characters (mirrors validateGRPCService’s length cap — the gRPC
health-checking service name is not DNS-1123 formatted, but admission still
bounds its length), and grpc.port is always numeric regardless of any
kind’s named-port rules — a named grpc.port is rejected outright with its
own message, never resolved against a declared container port; a key other
than port/service inside a probe grpc object (e.g. a misspelled
servcie) is rejected outright too, instead of silently ignored;
tcpSocket.host, if authored, is preserved on the probe — a
present-but-non-string value (e.g. host: 123) is rejected
(parseStringField), instead of silently
discarded while the probe still dials the Pod IP; when omitted,
corev1.TCPSocketAction.Host’s own doc comment says it then defaults to the
Pod IP, so no explicit default needs to be authored here — the same
optional-override shape as httpGet.host below; a key other than
port/host inside a probe tcpSocket object (e.g. a misspelled hots)
is rejected outright too, instead of silently ignored;
httpGet.httpHeaders itself and its entries are validated
rather than silently dropped/coerced — an authored non-array httpHeaders
value (e.g. a single header object instead of a list) is rejected the same
as a non-object entry within it, a missing/empty/invalid name
(validation.IsHTTPHeaderName, matching validateHTTPGetAction), or a
present-but-non-string value are all rejected instead of quietly
disappearing or turning into ""; a key other than name/value in a
header entry (e.g. a misspelled vaule) is rejected outright too, rather
than being silently ignored; an omitted value key still defaults to
"" — shared by lifecycle.{postStart,preStop}.httpGet below via the same
parsing helper; a key other than port/path/host/scheme/httpHeaders
anywhere in an httpGet object (e.g. a misspelled pth for path) is
rejected outright too, instead of silently ignored while Kubernetes defaults
whatever field the typo was meant to override — the identical five-key
allow-list is checked independently in lifecycle.{postStart,preStop}.httpGet
below, which has its own copy of this handler; path/host/scheme are
all optional — an absent or empty
path matches real Kubernetes’ own defaulting (SetDefaults_HTTPGetAction
fills it with "/" before validation ever runs, wired for both probe and
lifecycle httpGet handlers), so this schema does not require what upstream
itself fills in; host has no format validation in real admission either
(only rejected in combination with an HTTP2 protocol, which this schema does
not expose), so an unusual-looking host string is accepted verbatim, same as
real admission; scheme, if authored, must be HTTP or HTTPS
(case-insensitive); all three, plus every probe numeric field below, reject a
present-but-wrong-type value instead of silently treating it as absent;
initialDelaySeconds/periodSeconds/timeoutSeconds/successThreshold/
failureThreshold/terminationGracePeriodSeconds are typed integers, each
bounds-checked to match real Kubernetes admission (validateProbeTimeouts
plus the liveness/startup successThreshold rule) rather than accepting any
integer at face value: initialDelaySeconds must not be negative;
periodSeconds/timeoutSeconds/successThreshold/failureThreshold must
each be at least 1 (a periodSeconds: 0, for example, would otherwise author
a probe Kubernetes itself would reject); successThreshold must additionally
be exactly 1 on a liveness or startup probe — only a readiness probe may set
it above 1, since liveness/startup have only two outcomes (still healthy,
or restart) and “N consecutive successes to reset that state” has no defined
meaning for either —
terminationGracePeriodSeconds is additionally rejected outright on a
readiness probe (a failed readiness check only pulls the pod from Service
endpoints, it never terminates the container, so the field has nothing to
apply to there) and must be at least 1 when set on a liveness or startup
probe), lifecycle (rejected outright if authored with a non-object value,
e.g. a scalar or array, instead of silently treating it as absent and
running the container with no startup/shutdown hooks; a key other than
postStart/preStop (e.g. a misspelled lifecycle: {postStop: {...}}) is
rejected outright too, rather than matching neither recognized key and
silently producing no hook at all;
postStart/preStop:
exec (every command element must be a string; a non-string element is
rejected, not silently dropped; a key other than command in the object,
e.g. a misspelled commnad, is rejected outright too, instead of silently
ignored)/httpGet (same named-port, httpHeaders,
unknown-key, and optional-path/host/scheme rules as probes
above)/sleep (seconds is required and must be a non-negative integer; a
key other than seconds in the object, e.g. a misspelled sconds, is
rejected outright too, instead of silently ignored) — at
most one of these three may be authored, and a present-but-non-object value
for any of them is rejected outright rather than silently discarded while a
well-formed sibling wins, same as probes above;
tcpSocket is rejected unconditionally, even when paired with another valid
handler such as exec, and regardless of its own value’s shape (an
authored-but-malformed tcpSocket, e.g. a string, is rejected the same as a
well-formed one) — corev1 documents it as broken for lifecycle hooks,
and simply ignoring the extra key would silently drop the authored
tcpSocket while emitting only the other handler; a key on a postStart/
preStop value outside httpGet/exec/sleep/tcpSocket itself (e.g. a
misspelled timeoutSeconds alongside a valid exec) is rejected outright
too, rather than being silently ignored while the valid sibling handler
still builds; postStart/preStop are
each rejected outright if present with a non-object value, e.g. preStop: "flush", instead of silently discarding the whole hook),
securityContext (rejected outright if authored with a non-object value,
e.g. a scalar or array, instead of silently treating it as absent and
emitting a container with a nil security context; a key other than the
eleven recognized security-context fields (e.g. a misspelled
readOnlyRootFileSystem) is rejected outright too, instead of matching none
of them and silently discarding the whole hardening request;
per-container: runAsUser/runAsGroup/runAsNonRoot
(runAsUser: 0 combined with runAsNonRoot: true builds and is admitted by
the API server, but the kubelet’s verifyRunAsNonRoot check deterministically
fails it at container-start time — a CreateContainerConfigError, every
time — so this contradictory combination is rejected here instead of
shipping a workload guaranteed never to start; runAsUser/runAsGroup are
each rejected if authored with a non-integer value, e.g. a quoted "1000",
rather than silently omitting the UID/GID — since the container would then
fall back to the image’s own default, which may be root, while looking like
the authored value was honored; both must also be non-negative),
readOnlyRootFilesystem, allowPrivilegeEscalation, privileged (these
three, plus runAsNonRoot above, are each rejected outright if authored with
a non-boolean value, e.g. a quoted "false", instead of being silently
skipped: since Kubernetes’ default for each is permissive, silently dropping
a mistyped value would leave the container permissive while looking like the
authored hardening request was honored; additionally, privileged: true
combined with allowPrivilegeEscalation: false is rejected outright —
corev1.SecurityContext.AllowPrivilegeEscalation’s own field doc states it
is always true once a container runs privileged, so the pair would claim a
hardening guarantee the runtime cannot honor, the same contradiction shape as
runAsUser: 0 with runAsNonRoot: true above),
capabilities (rejected if authored with a non-object value; a key other
than add/drop in the object, e.g. a misspelled dorp, is rejected
outright too, instead of silently ignored; add/drop
are each rejected if authored with a non-array value, e.g. drop: ALL
instead of a list, or with a non-string array element — an empty-string
element is silently skipped rather than rejected, since real admission places
no format constraint on a Capability string at all. Adding the literal
capability CAP_SYS_ADMIN alongside allowPrivilegeEscalation: false is
rejected outright — Kubernetes admission always treats a container holding
that capability as privilege-escalated regardless of the field’s own value,
so the combination promises hardening the runtime cannot honor; the
unprefixed conventional form SYS_ADMIN is not rejected, matching real
admission’s own exact-string scope (it checks only CAP_SYS_ADMIN,
literally). add is checked against the environment policy’s
AllowedContainerCapabilities/ForbiddenContainerCapabilities accessor pair
(enforceContainerCapabilities in enforce.go) — a separate pair from
oam.Policy’s AllowedCapabilities/ForbiddenCapabilities/
RequiredCapabilities, which gate OAM trait-type usage (e.g. “ingress”), not
container Linux capability strings (e.g. “NET_ADMIN”); see enforce.go’s
enforcePrivileged doc comment for the naming-collision detail.
Default-allow, forbidden-list-first semantics: a nil/empty Allowed list
means no restriction and a nil/empty Forbidden list means no forbids, but a
capability present in both is rejected — forbidden always wins. drop is
never checked against policy — dropping a capability is strictly hardening.
Both the authored value and every policy-list entry are normalised
(upper-cased, CAP_ prefix stripped) before comparison, so NET_ADMIN,
CAP_NET_ADMIN and net_admin are treated as the same capability on both
sides; ALL is special-cased symmetrically on both sides. An authored entry
that normalises to ALL is rejected whenever Forbidden is non-empty, even
if no entry normalising to ALL is itself listed in Forbidden, since ALL
necessarily grants every forbidden capability. Conversely, a Forbidden
entry that normalises to ALL rejects every authored add entry
unconditionally — forbidden: ["ALL"] means no capability may be added at
all, regardless of what Allowed says), seccompProfile (rejected outright if authored with
a non-object value, e.g. seccompProfile: RuntimeDefault, instead of silently skipping the field
and dropping the requested sandboxing entirely; a key other than
type/localhostProfile in the object, e.g. a misspelled locahost, is
rejected outright too, instead of silently ignored; type is required whenever the
seccompProfile object is authored at all, matching real admission’s own
field.Required — omitting it (e.g. authoring only localhostProfile with
no type key) is rejected rather than silently discarding the whole
profile; localhostProfile is rejected outright when authored alongside
type: RuntimeDefault/Unconfined (only meaningful for type: Localhost),
including a present-but-non-string value in that position (e.g.
localhostProfile: 123) — the present-but-wrong-type value is rejected
(parseStringField), not silently treated as
absent while the contradictory type is accepted as authored; when type is
Localhost, localhostProfile must be
relative and must not contain a .. backstep component, matching
corev1.SeccompProfile.LocalhostProfile’s own doc comment — “must be a
descending path, relative to the kubelet’s configured seccomp profile
location” — and this repo’s own path-safety convention), seLinuxOptions
(also rejected outright if authored with a non-object value, same reasoning as
seccompProfile above; a key other than user/role/type/level in the
object, e.g. a misspelled tpye, is rejected outright too, instead of
silently ignored;
user/role/type/level are each rejected if authored with a
non-string value, e.g. type: 123, instead of silently discarding just that
sub-field — if it were the only one set, the whole SELinux context would
otherwise vanish rather than reporting the malformed input),
appArmorProfile (same “type required when authored” rule as
seccompProfile above, the same non-object rejection as seccompProfile
and seLinuxOptions, the same unknown-key rejection (only
type/localhostProfile are recognized) as seccompProfile above, and the
same localhostProfile
mutual-exclusivity-with-RuntimeDefault/Unconfined and present-but-non-string
rejection as seccompProfile above), procMount (Default|Unmasked; a present-but-non-string
value, e.g. procMount: false, is rejected rather than silently omitted
(parseStringField); an explicit empty
string is still treated as absent, not an error, which is parseStringField’s
own convention), windowsOptions (gmsaCredentialSpecName,
gmsaCredentialSpec, runAsUserName and hostProcess, the closed key set the
pod-level podSecurityContext.windowsOptions reads, go-kure/launcher#790; an
os.name: linux pod refuses it, as it refuses the pod-level one. Its strings
are held to upstream’s validateWindowsSecurityContextOptions, at both
levels: gmsaCredentialSpecName is a DNS-1123 subdomain, gmsaCredentialSpec
is at most 64 KiB, and runAsUserName is USER or DOMAIN\USER with no
control character, a domain under 256 characters in the NetBIOS or the DNS
format, and a user of at most 104 characters that is not only periods and
spaces and holds none of "/\:;|=,+*?<>@[]; an empty string is absent, by the
same convention (breaking, go-kure/launcher#790: a pod-level value that
breaks these rules built before and is refused now). Upstream’s validateWindowsHostProcessPod rules are held on the assembled pod, so they
are reported at Generate: a container’s hostProcess must equal the pod’s
when both are set; a pod with one HostProcess container — its own
hostProcess, or the pod’s when it sets none — must have only HostProcess
containers, init containers included; and such a pod must set
hostNetwork: true. hostProcess: true is policy-gated, see below)),
workingDir (a bare pass-through — corev1.Container.WorkingDir’s own
doc comment states only that the container runtime’s default applies when
unset, and real admission enforces no path shape for it, so this schema does
not invent a stricter constraint upstream itself does not have; a
present-but-non-string value, e.g. workingDir: 123, is rejected rather than
silently treated as absent, in all seven kind handlers — this is a type check,
distinct from the content-validation question the previous paragraph answers),
volumes (the volumes property itself, if authored, must be an array — a
present-but-non-array value, e.g. volumes: {name: data}, is rejected
outright rather than silently treated as absent and building without the
requested volume/mount, the same presence-then-type-check shape as probes
above; each entry in that array must itself be an object — a non-object
entry, e.g. volumes: [data], is rejected outright rather than silently
skipped while any well-formed sibling entries still build; every volume’s
name, regardless of source type — hostPath,
emptyDir, pvc, configMap, secret, etc. — must be a valid DNS-1123
label, matching how real admission validates every corev1.Volume.Name; an
invalid name, e.g. containing /, builds successfully but is rejected at Pod
admission; two volumes sharing the same valid name are likewise rejected —
Pod volume names must be unique (validateVolumes’ own duplicate check),
so the second entry is caught at parse time rather than only at admission;
two volumes with distinct names sharing the same mountPath are also
rejected — a container’s own volume mounts must have unique mount paths
(ValidateVolumeMounts’ own duplicate check), so only the first of two
colliding mounts would ever actually be reachable at admission; readOnly,
if authored, must be a boolean — a present-but-non-boolean value (e.g.
readOnly: "true") is rejected rather than silently defaulting to a
writable mount (same fix applied to initContainers/sidecars’ own
volumeMounts entries below, which had the identical gap);
name and mountPath are both required on every entry — the one exception
is a pvc entry with volumeMode: Block, which authors devicePath instead
(see “Raw block volumes” below) — a present-but-
non-string value (e.g. a numeric mountPath) is a type error; an entry missing
either, or authoring one with the wrong type, previously built with no
volume and no mount for that entry instead of reporting what was missing;
emptyDir.sizeLimit, if authored, is parsed as a
resource.Quantity and rejected if negative (e.g. "-1Gi") — syntactically
valid but a storage quantity real Kubernetes resource validation refuses,
same as resources’ own quantity fields above; a present-but-non-string
value (e.g. sizeLimit: 1048576) is rejected the same way, instead of
failing the bare type assertion silently and building an emptyDir with no
size limit at all. A zero sizeLimit is accepted on purpose: upstream
validateVolumeSource rejects only a negative value, and the kubelet applies
the limit only when it is greater than zero, so "0" means “no volume-level
limit” rather than an inadmissible quantity; pvc.size is required (the
same missing-vs-wrong-type rejection as name/mountPath above) and, once
present, must be a positive quantity. Behavior-changing
(go-kure/launcher#384): size: "0" used to build and is now rejected, because
ValidatePersistentVolumeClaimSpec runs ValidatePositiveQuantityValue over
requests[storage], which refuses zero as well as negative values — the
claim built before was never admissible. BuildPVC applies the same rule,
and the pvc trait, which parses its claim with the persistentvolumeclaim
kind’s ParseClaimProperties, refuses a zero or negative size too; pvc.storageClass,
if authored, must be a string — a present-but-non-string value (e.g. a bare
number) is rejected rather than silently building with the cluster default
class — and, once confirmed a string, a non-empty value must also be a valid
DNS-1123 subdomain, matching ValidatePersistentVolumeClaimSpec’s own
ValidateClassName check; a malformed class name (e.g. containing _ or
!) previously built successfully and was rejected only at admission. An
explicitly authored storageClass: "" is preserved as an
opt-out request (Kubernetes distinguishes a nil StorageClassName — use the
cluster default — from a pointer to "" — request no class) rather than
being collapsed to “absent” and silently provisioned through the default
class; volumeClaimTemplates.storageClass keeps the same distinction since
go-kure/launcher#761 (see “StatefulSet-level and claim-template properties”
below).
pvc.accessModes, if authored, must be a non-empty array of non-empty
strings, each one of the four real corev1.PersistentVolumeAccessMode
values — a present-but-non-array value (e.g. a bare string) or a non-string
element is rejected outright too, instead of silently falling through to the
ReadWriteOnce default while discarding the author’s actual list; an
authored empty array (accessModes: []) is rejected outright as well —
ValidatePersistentVolumeClaimSpec itself requires at least one access
mode, so silently defaulting an explicit empty list to ReadWriteOnce would
build a claim the author never asked for rather than reporting the
malformed input; an absent accessModes key still defaults to
ReadWriteOnce, unchanged — only an authored-and-empty array is treated as
malformed; ReadWriteOncePod
combined with any other access mode is rejected outright — real Kubernetes
requires it be the claim’s only mode; the same parser
backs volumeClaimTemplates.accessModes below; on webservice and
worker, the claim a pvc volume describes is named
<component name>-<pod-local volume name>, not the bare pod-local name — two
components in the same namespace that both author a pvc volume named
data would otherwise emit two colliding PersistentVolumeClaim/data
objects; the pod-local Volume.Name and its VolumeMount reference stay
unqualified, only the claim’s name and the matching
Volume.PersistentVolumeClaim.ClaimName are qualified (roleClaims in
role_members.go, once, when the rule lowers the component); the join
itself escapes interior hyphens in each half (escapeForPVCQualification)
before concatenating, since plain <appName>-<localName> string
concatenation is not collision-free when either half itself contains a
hyphen — component a-b with volume data, and component a with volume
b-data, would otherwise both qualify to a-b-data; a qualified name over
253 characters is shortened by the one rule (oam.ShortenNameWithSuffix,
go-kure/launcher#793), the escaped component cut and the escaped volume kept
whole, after its characters are checked on the unshortened name, so only the
length is forgiven; hostPath.path
is likewise required and must be absolute — a raw host filesystem path has
no defined root to resolve a relative value against, and real admission
(validateHostPathVolumeSource) rejects a relative one the same way;
configMap.configMapName and secret.secretName are each required the same
way as pvc.size; a fully-authored entry whose type matches none of the
five recognized sources — including type omitted entirely — is rejected
outright rather than silently producing no volume or mount for an entry the
author clearly intended to add; each recognized type’s own field set is
closed the same way securityContext and its nested objects are elsewhere
in this file — an unrecognized key on a hostPath, emptyDir, pvc,
configMap, or secret entry (e.g. a typo’d sizeLmit instead of
sizeLimit) is rejected rather than silently ignored, which previously let
the author’s intended value take no effect with no error explaining why),
initContainers, sidecars (each entry’s own volumeMounts[].readOnly
must be a boolean and volumeMounts[].subPath must be a string when
present — same presence-then-type-check shape as volumes.readOnly above;
volumeMounts[].mountPath must also be unique within that entry’s own
mount list, the identical rule as volumes’ duplicate-mountPath check
above, since each initContainers/sidecars entry is its own container;
each entry also accepts its own securityContext, the identical field set
and validation as the main container’s own securityContext described
below — see that prose for the field list rather than restating it here;
each entry is a closed key set (go-kure/launcher#321): an
initContainers entry accepts name, image, command, args, env,
envFrom, resources, volumeMounts, volumeDevices (see “Raw block
volumes” below), securityContext, workingDir and the seven keys of
“Container fields” below,
and a sidecars entry those plus ports, probes and lifecycle, each
parsed by the same parser as the main container’s field of that name. Any
other key is an error naming the entry — before that the parsers read the
keys they knew and dropped the rest, so an authored workingDir, envFrom,
probes or lifecycle built cleanly and reached no container. probes and
lifecycle on an init container are refused with their own message rather
than accepted: Kubernetes forbids both on an init container, which runs to
completion before the app containers start, and this package does not model
the restartable (restartPolicy: Always) init container that may carry them
— author them on a sidecar. A sidecar’s probe or hook may address a port by
name only when that sidecar itself declares a ports[] entry of that name —
the kubelet resolves a named port against the container’s own ports, so an
undeclared name would build and never resolve; unlike the main container,
whose single named port is fixed by its kind, a sidecar may declare several
and any of them is accepted. A sidecar’s ports list is read exactly as
deployment reads its main container’s (see “Main container ports” below):
closed entries, an IANA service name, a protocol from TCP/UDP/SCTP
(an empty string refused), and no repeated name or containerPort/protocol
pair within the sidecar (go-kure/launcher#660; before that the name and
protocol were taken unchecked, and a non-list ports, an unknown key, a
non-string name or a non-string protocol, which then read as TCP, was
dropped). A port name must also be unique across the
pod: the main container’s ports (webservice’s http, the ports list of
statefulset, daemonset and deployment) and every
sidecar’s. The
API server checks names only per container and merely warns across them,
while a Service selecting the name reaches only the first container that
declares it, so a repeated name is refused, naming both containers. Init
containers declare no ports, so they take no part. Closing the entry is behavior-changing under an
unchanged launcher.gokure.dev/v1alpha1: a document that authored any other
key on an entry built before and errors now. It is taken on the same
reasoning as the volumeClaimTemplates entry below — the key never reached
the output its author intended — and is signalled by the format commit
scope),
and affinity (four keys, parsed by parseAffinity: enablePodAntiAffinity
(boolean), topologyKey (string, default kubernetes.io/hostname),
podAntiAffinityType (string, preferred|required, default preferred)
and nodeSelector (a string→string map). Pod anti-affinity is emitted only on
an explicit enablePodAntiAffinity: true; authoring the block without it just
supplies the two defaults above, and omitting the block entirely leaves
topologyKey/podAntiAffinityType empty rather than defaulted (the defaults
are applied by parseAffinity only once the block is present, unlike the
postgresql component, which also tracks whether the block was authored at all).
The key set is closed (go-kure/launcher#790). The shorthand is not a
Kubernetes Affinity: nodeAffinity, podAffinity and podAntiAffinity
under it are each refused with what the shorthand has for the field and where
an affinity in the Kubernetes shape is authored (deployment, statefulset,
daemonset, job, cronjob), and any other key is refused as
affinity: unrecognized key "<key>", each whatever its value. The parser read
none of them before, so a caller that skips the document check had them
dropped; see
Upstream fields a hand-parsed kind does not read.
Each of the four sub-fields is read with a presence-reporting helper, so a
sub-field authored with the wrong type is rejected by name
(affinity.topologyKey: must be a string, got float64) rather than
discarded while the default above is emitted as though the key had never
been written, and a non-string nodeSelector value is refused by key
(affinity.nodeSelector["rack"]: must be a string, got int), the
alphabetically first offending key when several are wrong. A null sub-field
is absence and takes the default an omitted one takes, and a null
nodeSelector value is absence too: nodeSelector: {zone: a, rack: null}
builds the selector {zone: a} (before go-kure/launcher#466 it was refused
as must be a string, got <nil>). podAntiAffinityType
must be preferred or required: a well-formed string outside the enum is
refused by name, and an explicitly authored podAntiAffinityType: "" is an
error rather than a fall back to preferred — the empty string reaches the
enum check instead of being read as an absent key. An authored
topologyKey: "", by contrast, does fall back to the default. This is the
same contract the postgresql component’s own affinity block has
(go-kure/launcher#448). Behavior-changing under
launcher.gokure.dev/v1alpha1 (go-kure/launcher#452) for one authored shape:
the published schema leaves nodeSelector values untyped, so a document
with a non-string value (rack: 3) passed schema validation, built with that
entry dropped, and is now rejected. A wrongly typed sub-field itself was
already refused by schema validation for an authored document; the parser’s
own check now covers a caller that hands properties to a handler without it,
which previously got the default. The out-of-enum error now reads
affinity.podAntiAffinityType: invalid value … rather than
invalid podAntiAffinityType …).
On worker, the affinity the shorthand evaluates to is also validated the way the
deployment component validates a raw affinity, so a topologyKey, or a
nodeSelector key or value, that is not valid label syntax is refused
(affinity: the shorthand evaluates to an affinity the API server would refuse: …). Behavior-changing under launcher.gokure.dev/v1alpha1: such
a worker document built before, into a manifest the API server rejects, and
is now refused at build time — a pre-GA format change (docs/oam/design-gvk.md
§ Document-Format Lifecycle), signalled by the fix(format) commit scope. The
check runs after every other worker property is parsed, so an earlier refusal
keeps its place. A component name longer than 63 characters stays refused;
when the shorthand enables pod anti-affinity, whose label selector carries
the name, this check now refuses it before the container-name check does.
webservice validates the evaluated shorthand the same way, with the same
message, as the last step of its parse, as a pre-GA format change too
(a fix(format) change of its own): such a webservice document also built
before, into a Deployment the API server rejects. A webservice’s name is
checked against the Service-name rule first, so an over-long name is refused
there, before this check.
Container fields
Seven corev1.Container fields are read on every container of a hand-parsed
workload kind (go-kure/launcher#790): as top-level keys for the main container
of deployment, statefulset, daemonset, job and cronjob — and of
webservice and worker, which forward them to their deployment member — and
as keys of each initContainers and sidecars entry. One parser
(parseContainerFields, container_fields.go) and one schema fragment
(schemaContainerFields) serve the three positions, pinned to each other by
TestContainerFieldsSchemaMatchesParser.
| Property | Type | Effect |
|---|---|---|
imagePullPolicy | string: Always, IfNotPresent, Never | imagePullPolicy of the container. |
terminationMessagePath | string | terminationMessagePath, passed through: upstream validates no path shape for it, so none is invented here. |
terminationMessagePolicy | string: File, FallbackToLogsOnError | terminationMessagePolicy. |
stdin, stdinOnce, tty | boolean | The field of that name. Upstream has no rule relating the three, so none is applied. |
resizePolicy | list of {resourceName, restartPolicy} | resizePolicy. Both keys are required; resourceName is cpu or memory, restartPolicy is NotRequired or RestartContainer; a resource is named once. |
- Nothing is defaulted. A field that is not authored is not emitted, which
leaves the API server’s own defaults in force (for
imagePullPolicysee Every spec field is this package’s to write). An authoredfalseon a boolean is the field’s zero value and renders as absent too. - No policy check.
imagePullPolicy: NeverandIfNotPresentare accepted with no capability gate and noPolicymethod: a platform that requiresAlwayshas to enforce it at admission. - An explicit null is absence (see The null contract), and
so is an empty string on
terminationMessagePath(parseStringField’s convention) and an emptyresizePolicylist. An empty string onimagePullPolicyorterminationMessagePolicyis refused, as any value outside the enum is: by property validation in a document, and by the parser with the message below. The enum values are case-sensitive, as upstream:imagePullPolicy: alwaysis refused withimagePullPolicy: invalid value "always", must be one of Always, IfNotPresent, Never. - The
resizePolicyrules are upstream’s (validateResizePolicy,validateInitContainersink8s.io/kubernetespkg/apis/core/validation/validation.go), checked at build time so the author gets the entry named instead of an admission error:- an unknown key, a missing key, a resource other than
cpu/memory, an unknown restart policy and a repeated resource are each refused by entry (resizePolicy[1].resourceName: "cpu" is named by an earlier entry; …); RestartContaineris refused on aninitContainersentry: Kubernetes allows it only on a restartable init container, which this package does not model.NotRequiredis accepted there;- on a pod whose
restartPolicyisNever— which onlyjobandcronjobcan author — every container’s restart policy must beNotRequired. That check runs on the assembled pod spec (checkResizePolicyRestart), so it is reported atGenerate, not atToApplicationConfig.
- an unknown key, a missing key, a resource other than
- On an
initContainersorsidecarsentry every message carries the entry label first (sidecars[0] "proxy": tty: must be a boolean, got string).
An initContainers entry also reads its own restartPolicy and
restartPolicyRules (go-kure/launcher#790), held to upstream’s
validateInitContainerRestartPolicy and validateContainerRestartPolicy:
restartPolicyisNeverorOnFailure, overriding the pod’s restart policy for that container.Alwaysis refused with its reason: it makes the init container restartable (a native sidecar), which this package does not model, the same reasonprobesandlifecycleare refused on an init entry; author asidecarsentry instead;restartPolicyRulesneedsrestartPolicyand holds at most 20 rules. A rule is a closed object:actionis required and isRestart(upstream’sRestartAllContainersneeds a separate feature gate, and is refused), andexitCodesis required: a closed object whoseoperatoris required and isInorNotIn, and whose optionalvaluesis a list of at most 255 int32 exit codes, each once (the field is a set);- Kubernetes keeps either field on an init container only with the
ContainerRestartRulesfeature gate, on by default from Kubernetes 1.35. A cluster with the gate off drops both fields when it creates the pod, without an error, so the init container falls back to the pod’s restart policy. This package does not see the target cluster’s version, so that is a documented limit, not a check.
On a sidecars entry neither key is read, and both are refused as unknown
keys. At the top level neither is a container key either.
restartPolicyRules is refused there on every workload kind, with the reason
that upstream accepts a container’s rules only together with the container’s
own restartPolicy. restartPolicy there is the pod’s restart policy: a
key of job and cronjob, where the main container’s own restartPolicy
therefore has no name to be authored or refused under, and refused with its
reason on the other kinds, whose pod template apps/v1 validation holds to
Always. See
Upstream fields a hand-parsed kind does not read
for these and for the container fields read under another name (name, the
three probe fields, volumeMounts, volumeDevices).
Referencing an existing claim (pvc.claimName)
A pvc volume with claimName mounts an existing claim instead of generating
one (go-kure/launcher#702). The claim usually comes from a
persistentvolumeclaim component or a pvc trait, but it can be any claim in
the namespace. The volume generates no object, and its claim name is used
exactly as written: it is not qualified with the component name the way a
role kind’s claim is.
On the five pod kinds claimName is required. deployment,
statefulset, daemonset, job and cronjob generate no claim, so a pvc
volume without claimName is refused (volume "data": a pvc volume must set claimName to an existing claim; the component generates none, so declare it with a persistentvolumeclaim component or a pvc trait). This is a breaking
pre-GA format change. To migrate, declare the claim with a
persistentvolumeclaim component (or a pvc trait) carrying the volume’s
size, its storageClass as storageClassName, and its accessModes and
volumeMode. Then reference it from the volume with claimName. Keep
accessModes on the volume, and keep volumeMode: Block there too: a
devicePath mount needs it on the volume. The environment’s storage-size
limit and default now apply to that claim, not to the volume.
webservice and worker still accept a pvc volume that describes its
claim. The rule turns each one into a synthesized pvc trait on its
deployment member, named <component>-<volume> as before, and rewrites the
volume to reference it by claimName. The output is unchanged. The volume’s
claimObjectName names that claim instead (go-kure/launcher#787, role
workload-volume-claim; see “deploymentObjectName, serviceObjectName and
serviceAccountObjectName” below).
Such a volume that leaves storageClass unauthored (absent or null) takes
the ClusterProfile pvc capability’s storageClassName, as an authored pvc
trait and the persistentvolumeclaim kind do (go-kure/launcher#746). The
synthesized trait is sealed, so the engine merges no rendering into it; the
rule reads the binding through LoweringContext.Capability, which records
pvc in ConsumedCapabilities, and fills the value itself. An authored
class, "" included, wins. Pre-GA output change: such a volume used to
ignore the binding, so an existing claim built from it got the cluster’s
default class when it was created, if the cluster had one. A claim’s
assigned storageClassName cannot change: if the binding’s class differs
from it, the claim fails to apply until it is recreated or the volume
authors the assigned class. A claim created with no class may take one
later, so it applies. A statefulset volumeClaimTemplates entry takes the
same default (go-kure/launcher#761; see “StatefulSet-level and claim-template
properties”).
claimNamemust be a DNS-1123 subdomain.sizeandstorageClassare refused alongside it, because the referenced claim states its own (volume "data": size cannot be set with claimName; …).claimName: ""is refused, not read as absent.claimObjectNameis refused alongside it: it names the claim awebserviceorworkervolume generates, and this volume generates none.accessModesstays: it states the referenced claim’s modes, defaulting toReadWriteOnceas on a claim a role kind describes. The workload still reads them. A non-RWX claim still forcesstrategy: Recreateon a Deployment, still refusesreplicasabove 1, and still limits ascalerto one replica. To scale against aReadWriteManyclaim, stateaccessModes: [ReadWriteMany]on the volume.volumeMode,readOnlyand the mount keys work as on a described claim.
Every workload kind that accepts volumes accepts claimName. BuildPVC
refuses a PVCConfig that carries a ClaimName: there is no object to build.
Raw block volumes (volumeMode: Block)
A claim with volumeMode: Block has no filesystem. A container consumes it
through volumeDevices at a devicePath, never through volumeMounts
(go-kure/launcher#385). The claim and the pod template are validated as
separate objects, so the apiserver accepts a Block claim paired with a
filesystem mount and the pods then fail at kubelet mount time; every rule
below reports that mismatch at build time instead. The rules are the same on
all seven kinds (webservice, worker, deployment, statefulset,
daemonset, job, cronjob), because they live in the shared parsers.
volumes[]pvc entry. AddsvolumeMode(Filesystem|Block) anddevicePath. The entry takes exactly one ofmountPathanddevicePath, anddevicePathis authored if and only ifvolumeMode: Blockis — in both directions, with nothing inferred:mountPathwithBlock,devicePathwithFilesystemor with no mode, and both paths at once are each an error. A Block entry reaches the main container as acorev1.VolumeDeviceand has novolumeMount. The claim carries the authored mode; an unauthored mode stays unset (the apiserver defaults it toFilesystem), so an existing document’s claim is byte-identical.devicePathon any other volume type is an error.volumeClaimTemplates[]entry (statefulset). The same exactly-one rule withdevicePath, andvolumeMode: Blockis now accepted (it was rejected before). A Block template renders a claim template withvolumeMode: Blockand a main-containervolumeDevicesentry.- The published schema says “exactly one of”. A
volumes[]entry and avolumeClaimTemplates[]entry publishmountPathanddevicePathas two optional keys plus a requiredexclusivegroup of the two (go-kure/launcher#790), so a validator built from the schema refuses both and neither, as the build does. A key counts as authored when it is present and not null, an empty string included:mountPath: ""beside adevicePathis both, through the transform and through a direct handler call alike. initContainers[]/sidecars[]entry. AddsvolumeDevices: [{name, devicePath}], a closed key set with both keys required. AvolumeDevicesname must be a Block volume this component declares (avolumespvc entry or a claim template); avolumeMountsname must not be one. AvolumeMountsname the component does not declare at all stays accepted, because theconfigmapandexternal-secrettraits add their volumes after the component is generated — and neither adds a Block volume.- Name and path rules mirror
ValidateVolumeDevices, per container: a volume is named at most once involumeDevicesand not in bothvolumeMountsandvolumeDevices; adevicePathis unique, contains no..element, and is not also amountPath. Onstatefulsetthis spans the claim templates andvolumestogether, so a Block claim template cannot share its name with avolumesentry or another claim template. Theconfigmapandexternal-secrettraits likewise refuse a volume name the main container already attaches as a device — including a Block claim template, which has no pod volume of its own — and a mount at a path it already uses as adevicePath.
Compatibility. Additive: every new rejection concerns a Block volume,
and no document could declare one before (a volumes pvc entry had no
volumeMode, and a claim template’s Block was rejected). The one new rule
that reads existing keys — a volumeMounts entry naming a Block volume is an
error — can therefore only fire on a document that uses the new surface. The
published schemas drop Required from mountPath on volumes items and
claim templates; the parsers still require one of the two paths.
Pod-level properties
The seven kind components also share one pod-level property surface
(go-kure/launcher#342, ADR-036 L1), parsed by parsePodSpec (podspec.go)
straight into a corev1.PodSpec and rendered by the shared buildPodSpec,
which every kind assigns to its pod template. Property names are the
corev1.PodSpec JSON names, except three that carry a pod prefix because
the bare name is already a property at some kind: podSecurityContext
(container securityContext exists everywhere), podResources (container
resources), and podActiveDeadlineSeconds (cronjob’s job-level
activeDeadlineSeconds). Each accepted value is validated the way real
admission validates it when that check is deterministic from the document
alone (DNS-1123 names, enums, IP literals, duplicate names, the cross-field
exclusions listed below, and the os.name contract: a windows pod may not
set the Linux-only pod and container fields, a linux pod may not set
windowsOptions); checks needing cluster state (host ports under
hostNetwork, feature gates, RuntimeClass existence) are left to the cluster.
| Property | Type | Effect | Kind |
|---|---|---|---|
serviceAccountName | string | Behavior-changing. Pods run as the named account; on webservice and worker the per-component ServiceAccount is not generated. The rbac trait binds its Role/ClusterRole to this account via oam.ServiceAccountNamer (see below). Unset on a pod kind, the pod names no account (the namespace’s default), and the kind sets the pod-level automountServiceAccountToken: false unless that is authored (go-kure/launcher#702); a role kind’s generated account carries automountServiceAccountToken: false itself. An authored account is owned elsewhere and its own setting governs, so authors who want the pod not to mount a token set the pod-level automountServiceAccountToken: false explicitly — the handler does not inject it. | additive when unset |
automountServiceAccountToken | bool | Pod-level token automount override. | additive |
terminationGracePeriodSeconds | int ≥ 0 | Grace period before SIGKILL. | additive |
podActiveDeadlineSeconds | int 1..MaxInt32 | Pod-level activeDeadlineSeconds. cronjob and job only — apps/v1 rejects it on Deployment/StatefulSet/DaemonSet templates, so the other kinds neither publish nor accept it, and refuse the upstream name activeDeadlineSeconds with that reason (on job and cronjob that name is the JobSpec’s). Distinct from the JobSpec-level activeDeadlineSeconds below: this one bounds a single pod, that one the whole job. | additive |
dnsPolicy, dnsConfig | enum, object | ClusterFirstWithHostNet/ClusterFirst/Default/None; dnsConfig = nameservers (≤3 plain IPv4/IPv6 literals — a zone-scoped address such as fe80::1%eth0 is rejected, matching upstream’s net.ParseIP-based check, which has no notion of a zone), searches (≤32 entries whose joined length, separators included, is ≤2048 characters — the resolv.conf search-line limit, so 32 individually valid domains can still be refused), options[]{name,value}. None requires at least one nameserver. | additive |
nodeSelector, nodeName, schedulerName, priorityClassName, preemptionPolicy, runtimeClassName, schedulingGates[]{name}, schedulingGroup{podGroupName} | scheduling | Placement fields; gate names must be unique. nodeName is a DNS-1123 subdomain (a Node is an ordinary object, so an invalid value is refused at admission, not merely unmatched). schedulerName is deliberately not validated: upstream constrains its form nowhere, so an arbitrary string is a legal document and rejecting one here would refuse work a cluster accepts. | additive |
hostNetwork, hostPID, hostIPC | bool | Host namespaces. Policy-gated: rejected by ApplyPolicy unless AllowHostNetwork()/AllowHostPID()/AllowHostIPC() allow them (enforce.go’s enforceHostNamespaces, called from all seven kinds; NoopPolicy denies all three). | additive |
shareProcessNamespace | bool | Mutually exclusive with hostPID: true. | additive |
hostname, subdomain, setHostnameAsFQDN, hostnameOverride, hostAliases[]{ip,hostnames} | naming | hostname/subdomain are DNS-1123 labels; hostnameOverride is a ≤64-char subdomain and cannot combine with hostNetwork or setHostnameAsFQDN. hostAliases[].ip is a plain IPv4/IPv6 literal, zone suffixes rejected as for dnsConfig.nameservers. | additive |
podSecurityContext | object | The full corev1.PodSecurityContext field set (runAsUser/runAsGroup/runAsNonRoot/fsGroup/fsGroupChangePolicy/supplementalGroups/supplementalGroupsPolicy/sysctls/seLinuxOptions/seLinuxChangePolicy/seccompProfile/appArmorProfile/windowsOptions), closed and validated like the container securityContext; the windowsOptions strings are held to upstream’s rules as on a container (see “Container fields”). sysctls[].name must match the sysctl grammar (≤253 characters of dot- or slash-separated lowercase alphanumeric segments) and be unique within the list. windowsOptions.hostProcess: true additionally requires hostNetwork: true, which upstream demands of any pod containing HostProcess containers. The runAsUser: 0 / runAsNonRoot: true contradiction is judged per container on the effective values once the containers are assembled, not on this object alone — a container-level runAsUser overrides the pod-level one, so the pair is a valid document when every container names a non-root UID, and the deferred check also catches a container-level runAsUser: 0 under a pod-level runAsNonRoot. Partly policy-gated: windowsOptions.hostProcess: true is rejected unless AllowPrivileged() allows it (a HostProcess pod runs with the node’s own privileges, and upstream forces every container in it to be HostProcess too); every other field has no policy hook. | additive |
imagePullSecrets[]{name}, enableServiceLinks, os{name}, hostUsers, readinessGates[]{conditionType}, resourceClaims[]{name, resourceClaimName | resourceClaimTemplateName}, podResources{requests,limits} | misc | podResources accepts only cpu, memory and hugepages-<size> (pod-level resources have no ephemeral-storage or extended resources), and a hugepages-<size> entry needs cpu or memory in requests or limits (admission’s “HugePages require cpu or memory”; pod-level resources get no defaults); claim names must be unique and name exactly one source (an empty name beside the other counts as a second; the schema publishes the pair as an exclusive group, go-kure/launcher#790). hostUsers: false cannot combine with hostPID or hostIPC (upstream forbids both outright); it stays authorable alongside hostNetwork, which upstream forbids only on a cluster without user-namespace host-network support, so whether that pair is accepted is a property of the target cluster rather than of the document. podResources is policy-gated: its cpu and memory requests and limits are checked against MaxCPU()/MaxMemory(), the same budget the container resources are checked against. | additive |
Deliberately not accepted — each is rejected with an error naming the
reason rather than silently ignored: ephemeralContainers (added to a running
pod through its ephemeralcontainers subresource, never on a template),
priority and overhead (the Priority and RuntimeClass admission
controllers, on by default, derive them from
priorityClassName/runtimeClassName and reject a pod whose authored value
differs, so authoring one can at best repeat the derived value),
serviceAccount (deprecated alias of serviceAccountName),
evictionResponders (alpha upstream, behind the EvictionRequestAPI feature
gate) and containers (the pod’s one main container is the component’s own
container properties, and further ones are sidecars entries where the kind
has them).
Every pod kind config implements oam.ServiceAccountNamer (pkg/oam/handler.go),
returning (name, runsPods): the authored serviceAccountName (or "") and
true. A role rule hands its deployment member the name of the account its
serviceaccount member generates (the component name, or the one
serviceAccountObjectName or the Naming hook gave that object) unless
serviceAccountName is authored, so a webservice or worker answers the
account it generates. The rbac trait
(pkg/oam/builtin/traits/rbac.go) binds its RoleBinding/ClusterRoleBinding
subject to that name, so binding follows the account the pods run as; the Role
and binding objects keep their component-derived names. A pod-running
component with no account name is refused by rbac rather than bound to an
account that does not exist; see the traits README.
The pod’s serviceAccountName is the namer’s own answer, so the account a
RoleBinding names and the account the pods run as are one value by
construction. The shared kind test asserts they agree on every kind, including
against a deliberately mismatched Application name, and that a role kind’s
generated ServiceAccount carries the same name.
securityContext.privileged: true is rejected unless the environment policy’s
AllowPrivileged() allows it (enforce.go’s enforcePrivileged).
securityContext.capabilities.add is separately enforced against
AllowedContainerCapabilities()/ForbiddenContainerCapabilities() (see above,
enforce.go’s enforceContainerCapabilities). enforcePrivileged gates a
second field besides privileged: securityContext.windowsOptions.hostProcess
is rejected under the same AllowPrivileged(), on any container that authors
it (go-kure/launcher#790 made the container windowsOptions authorable).
Those three fields are the whole container-level policy surface:
enforcePrivileged and enforceContainerCapabilities are the only enforcers
taking a *corev1.SecurityContext, and every policy call outside enforce.go
that touches a container securityContext is a call site of one of them. The
package has other enforcers reached from the same ApplyPolicy bodies — the
pod-level pair described below, plus enforceHostNamespaces,
enforceHostPathVolumes, the enforceMax* family and
enforceAllowedRegistries/enforceAllowedURLHosts — but none of them reads a
container securityContext. Both checks cover the main container and
every initContainers/sidecars entry (go-kure/launcher#312’s shared
enforceExtraContainer helper), not just the main container.
The pod-level podSecurityContext has a separate hook —
enforcePodHostProcess (enforce.go) rejects
podSecurityContext.windowsOptions.hostProcess under AllowPrivileged(). It is
a different object, not an exception to the container-level list above:
PodSpecConfig embeds corev1.PodSpec, so its SecurityContext is a
*corev1.PodSecurityContext, which enforcePrivileged never sees. Both
spellings are authorable — see the podSecurityContext row under “Pod-level
properties”.
The pod-level surface carries two policy checks of its own, both called from
all seven kinds’ ApplyPolicy next to enforceHostNamespaces:
enforcePodResources measures podResources’ cpu and memory requests and
limits against MaxCPU()/MaxMemory() — without it an author could keep the
container under the maximum and put the oversized request on the pod, which
the scheduler charges against the node identically — and
enforcePodHostProcess rejects podSecurityContext.windowsOptions.hostProcess: true unless AllowPrivileged() allows it, the Windows spelling of
securityContext.privileged (enforcePrivileged checks the container-level
windowsOptions.hostProcess too, so the two spellings share one switch). An
unset MaxCPU/MaxMemory leaves pod-level resources unconstrained: pod-level
resources are written only when authored, so there is no generated default to
fall back on.
A volumes entry sourced from hostPath is rejected unless the environment
policy’s AllowHostPathVolumes() allows it (enforce.go’s
enforceHostPathVolumes, the same reused-mechanism shape as
enforcePrivileged above); like enforcePrivileged, it is called from all
seven kind components’ ApplyPolicy, not just one — a hostPath volume mounts
an arbitrary path from the node’s own filesystem into the Pod, so an
unenforced policy denial here is a container-escape-adjacent gap, not merely
a style one. The same switch gates a PersistentVolume’s hostPath and local
sources, on every path that produces the object (see persistentvolume
below): both name a path on the node, which a pod reaches by binding the
volume through a claim.
Setting any securityContext field makes the container’s SecurityContext
non-nil, which opts it out of the security-context trait’s nil-only
backfill for every other SecurityContext field too. If a component uses
both, the trait’s Generate() pass runs later and unconditionally overwrites
container.SecurityContext — the trait always wins when both are applied to
the same component. This applies to a component-authored initContainers/
sidecars entry’s own securityContext too, not just the main container’s:
the trait’s applyToPodSpec (pkg/oam/builtin/traits/security_context.go)
overwrites SecurityContext on every entry of both podSpec.Containers
(which includes rendered sidecars) and podSpec.InitContainers, so it wins
over an init container’s or sidecar’s own authored value the same way it
wins over the main container’s. Use the trait for a safe, complete
PSA-consistent default; use this property for raw, partial, full-fidelity
authoring.
env, envFrom, resources, lifecycle, securityContext, workingDir, and
probes are each schema fragments parameterized by a reserved bool (mirroring
pkg/oam/builtin/traits/schema.go’s schemaNetworkPolicy(reserved bool)):
every built-in call site passes false today. Deciding which of these fields
should be platform-reserved (rejecting any authored value via
PropertySchema.PlatformReserved/enforcePlatformReserved) is a consumer-side
policy choice, not something this shared schema hardcodes.
Deployment-level properties (deployment, webservice, worker)
Three components produce an appsv1.Deployment: the kind-named deployment
(go-kure/launcher#343; ADR-036 L1, the stratified-levels decision: one
PodSpec/Container projection shared by every kind, with kind-named components
projecting their own API kind), and the two role-named kinds webservice
(Deployment + Service) and worker (Deployment, no Service). Everything above
— the container-level surface and the pod-level surface — applies to all three
unchanged, and so does the rest of DeploymentSpec below: it is parsed by
parseDeploymentSpec (deployment_spec.go) into real appsv1 types and
written by its apply onto the built object, from one fragment all three
handlers compose (go-kure/launcher#341).
Sharing the fragment is the point rather than a convenience. These fields are
properties of the API kind, not of the role a component plays, so a document
that moves a workload from webservice to deployment — or the reverse —
should not lose a strategy on the way. Every rule the table below states,
including the refusals, therefore holds identically on all three.
The three kinds are still not layered, and one is not a superset of the others.
deployment deliberately publishes no port (and so emits no Service),
no affinity shorthand and no default topology-spread constraint —
none of those is a DeploymentSpec field, and a kind named after the API kind
should project the API kind rather than launcher’s opinions about it. A
workload that wants launcher to create its Service uses webservice. This is
the reversible direction: adding a property later is additive, removing one is
breaking. Its ports list (below) is that kind of addition: container ports
are a PodSpec field, and declaring them emits no Service.
What deployment does publish, and the role kinds do not, is the raw
corev1 form of the same three scheduling concerns — see “Raw scheduling
properties” immediately below. The two are not alternatives: the shorthand is
an opinion, the raw shapes are the API.
The default topology-spread opinion is still available on deployment, as an
explicit opt-in rather than a kind default: the topology-spread trait (see
the trait handlers README) applies the same
BuildTopologySpreadConstraints the role kinds use, exported for that reason,
from the Deployment’s post-policy replica count. It refuses a Deployment that
already carries raw topologySpreadConstraints, so the two never merge.
Main container ports
deployment publishes ports, the main container’s corev1.ContainerPort list
(go-kure/launcher#280). It declares container ports only: the kind still emits
no Service (use webservice, or a service component, for one).
daemonset, statefulset, job and cronjob publish the same ports, with
the same entry rules (go-kure/launcher#334): container ports are a PodSpec
field, so every kind-named workload component projects them. ports emits no
Service on any of them. A named probe or hook port resolves against the
main container’s ports; with none declared, it is refused.
Breaking (go-kure/launcher#690): daemonset and statefulset dropped port
and the Service it drove, as deployment did in go-kure/launcher#343. port
declared the main container’s first port and a Service in front of it: a
ClusterIP Service named after the component on daemonset, a headless one
named serviceName (default: the component name) on statefulset. An authored
port is now refused as an unknown property. To migrate:
- Move the number into
portsunder the nameportgave it, so named probe and hook ports keep resolving:port: Nbecomesports: [{name: http, containerPort: N}]ondaemonset,ports: [{name: tcp, containerPort: N}]onstatefulset. - Author the Service as a
servicecomponent that selects the pods (selector: {app: <component>}). For astatefulset’s governing Service, setclusterIP: Noneand name it inserviceName. serviceNameno longer defaults to the component name: unset, the StatefulSet carriesserviceName: "". The API server refuses a change tospec.serviceNameon an existing StatefulSet, so one that relied on the default and now names the authored Service must be recreated: delete it with--cascade=orphanso its pods survive, then apply. The adopted pods keep the oldspec.subdomain, which is set only when a pod is created, so their DNS names under the new Service do not resolve until they are replaced: runkubectl rollout restart statefulset/<name>(or, underupdateStrategy: OnDelete, delete the pods one at a time). ArollingUpdate.partitionexcludes the pods below it from the restart: set it to0for the restart, or delete those pods one at a time. (The authored Service cannot keep the old name: it is the component’s own, and component names are unique within an Application.)- A trait that routed to the component’s own Service (an implicit
ingress,httprouteorexposebackend, or anetworkPolicyingress allow) now belongs on the authoredservice; on the workload it is refused withcomponent "<name>" has no service port.
examples/13-statefulset.yaml shows the result.
| property | type | notes | compat |
|---|---|---|---|
ports | array | Each entry is containerPort (required, 1–65535), name (an IANA service name, as the API server checks a container port name) and protocol (TCP/UDP/SCTP, default TCP; an empty string is refused, as the published enum refuses it); any other key, hostPort and hostIP included, is refused. Names must be unique, as the API server requires, and also unique across the pod: a sidecar port of the same name is refused (go-kure/launcher#660). A repeated containerPort/protocol pair is refused too, which the API server only warns about: the second entry declares nothing new. The same number on two protocols is two ports. An absent, null or empty list declares no ports. A probe or lifecycle hook may address a declared port by name; a name the main container does not declare is refused, since the kubelet resolves it only against that container’s own ports. Without ports, a named probe or hook port is refused. | additive (daemonset/statefulset: replaces port, go-kure/launcher#690) |
Raw scheduling properties
deployment publishes affinity, tolerations and
topologySpreadConstraints as their plain corev1 shapes
(go-kure/launcher#412), parsed by scheduling.go. Nothing is inferred from the
component: every selector, weight and topology key is authored.
The other kind-named workloads publish the same shapes through the same parsers and schemas, so every rule in the table below holds wherever the key is published (go-kure/launcher#790):
| kind | affinity | tolerations | topologySpreadConstraints |
|---|---|---|---|
deployment | raw | yes | yes |
statefulset | raw | yes | yes |
daemonset | raw | yes | yes |
job | raw | yes | yes |
cronjob | raw | yes | yes |
Breaking (go-kure/launcher#790): statefulset dropped the four-key affinity
shorthand. Its affinity is the raw corev1 shape, as on the other kinds in
this table, so enablePodAntiAffinity, topologyKey, podAntiAffinityType and
nodeSelector under it are refused as unrecognized keys. To migrate, author
what the shorthand built: a podAntiAffinity term selecting app: <component>
(see The app label) over the topology key, preferred with
weight 100 or required; and, for nodeSelector, a required nodeAffinity
with one In requirement per label.
- Nothing is defaulted on any of them: an unauthored key emits nothing, and a
constraint or an affinity term selects only the pods its authored
labelSelectornames. The pods a kind builds carryapp: <component>(see Theapplabel). - No policy check.
tolerationsis accepted with no capability gate and noPolicymethod on every kind that publishes it, so a component can tolerate any taint, a control-plane node’s included; a platform that reserves nodes by taint has to enforce that at admission. - The
topology-spreadtrait stays Deployment-only. It is refused on astatefulset, adaemonset, ajoband acronjob, with or without authored constraints (component "<name>" generates no Deployment the trait can act on). - On a
cronjobthe three keys are written onto the pod template of the Job template (spec.jobTemplate.spec.template.spec), so they apply to the pods of every Job the CronJob creates. - On a
daemonsetthe three keys reach the DaemonSet’s pod template as authored. The DaemonSet controller creates one pod for each node the template’s node affinity, node selector and tolerations admit, and pins the pod to that node; a pod affinity term or aDoNotSchedulespread constraint the node cannot satisfy leaves that pod pending instead of placing it elsewhere. Nothing here checks for that.
| property | type | notes | compat |
|---|---|---|---|
affinity | object | nodeAffinity, podAffinity, podAntiAffinity, each with the requiredDuringSchedulingIgnoredDuringExecution / preferredDuringSchedulingIgnoredDuringExecution arms. Node requirement operators are In/NotIn/Exists/DoesNotExist/Gt/Lt, with upstream’s arity rule — In/NotIn need at least one value, Exists/DoesNotExist none, Gt/Lt exactly one integer. matchExpressions keys are node label keys (qualified names). matchFields keys are not qualified names and are not free-form field paths either: metadata.name is the only key Kubernetes accepts, and a field requirement takes only In or NotIn with exactly one value — the arity rule above belongs to matchExpressions, and a matchFields entry it would accept can still be refused at admission. That single value is a node name and is validated as a DNS-1123 subdomain: the key resolving is what arms the value rule upstream rather than ending it, so a well-formed key and operator with a malformed value is still refused. matchFields therefore publishes its own item schema rather than sharing matchExpressions’, with key and operator enums matching the parser — an emitted property is checked against the schema’s enums before conversion, so a looser schema would advertise operators to a lowering rule that the parser then rejects. Weights must be 1–100. An affinity with no arm set is rejected, as is a node selector term with neither matchExpressions nor matchFields — upstream documents such a term as matching no nodes, so it can only be a mistake. The published schema marks nodeSelectorTerms, and a weighted arm’s preference and podAffinityTerm, as required — the parser already refused each of them being absent, so this changes what schema introspection reports, not what builds. | additive |
tolerations | array | The same property daemonset already publishes, from the same parser, now covering the complete corev1.Toleration: key, operator (Exists/Equal/Lt/Gt), value, effect, tolerationSeconds. tolerationSeconds is a pointer upstream, so unset (tolerate forever) and 0 (evict immediately) are different documents. A non-empty key must be a qualified name; the empty key stays legal and is governed by the rule below. Cross-field rules: an empty key requires Exists; a value under Exists is refused; under Equal the value must be a valid label value, since it is matched against a taint’s; Lt/Gt need a canonical decimal integer value (no leading zeros, no plus sign, no -0 — Toleration.ToleratesTaint runs content.IsDecimalInteger before parsing and silently matches nothing otherwise) and the cluster’s TaintTolerationComparisonOperators gate; tolerationSeconds requires effect: NoExecute spelled out, an omitted effect not counting as a wildcard for this rule. An unrecognised key is reported rather than dropped, and so is tolerations authored as something other than an array. | additive for deployment; narrowing for daemonset — see below |
topologySpreadConstraints | array | maxSkew (required, > 0), topologyKey (required), whenUnsatisfiable (required, DoNotSchedule/ScheduleAnyway), labelSelector, minDomains (> 0, and only with DoNotSchedule), nodeAffinityPolicy/nodeTaintsPolicy (Honor/Ignore), matchLabelKeys. Two constraints may not repeat the same (topologyKey, whenUnsatisfiable) pair; sharing a topologyKey with different whenUnsatisfiable values is legal and stays accepted. The three required fields carry no omitempty upstream, so an unset one would emit maxSkew: 0 / topologyKey: "" / whenUnsatisfiable: "" rather than an API default — hence required here rather than defaulted. | additive |
What tolerations changed for daemonset
affinity and topologySpreadConstraints are new properties on a kind that did
not have them, so they are additive outright. tolerations is not: deployment
reaches it through the same parseTolerations/schemaTolerations pair that
daemonset has always used, and those two kinds were its only callers then, so
completing the projection changed daemonset too. (A kind that gained
tolerations later, statefulset, job and cronjob in go-kure/launcher#790, had no earlier
behaviour to change: for it the property is additive.) Stating that plainly, per
rule, because “additive” on its own would be false:
| change | effect on daemonset | why it is kept rather than gated to deployment |
|---|---|---|
A well-formed tolerationSeconds is now read | Additive in capability, and the one change that alters emitted bytes. The key was previously accepted and silently dropped, so a daemonset that already authored it emitted a toleration without an eviction deadline; it now emits the field it asked for. That is a deliberate output change, not a preservation. | It is the fix, not a side effect. |
A malformed tolerationSeconds is now an error | New errors only. A non-integer, fractional or out-of-int64-range value was previously ignored along with the rest of the key; it is now type-checked like any other integer property. | The key cannot be both read and unvalidated. A document carrying one was already not getting the deadline it asked for. |
operator: Lt and operator: Gt are now accepted | Purely additive. Both were previously refused as invalid operators; they are valid corev1 values (core/v1/types.go:4097-4100). Their value must be a canonical decimal integer — no leading zeros, no plus sign, no -0 — because the scheduler’s own matcher silently refuses to match any other form. | A “raw” projection whose operator set is narrower than the API’s is the wrong shape for either kind. |
| An unrecognised key inside a toleration entry is now an error | New errors only, and here output really is byte-identical. A key the parser never read contributed nothing to the emitted object, so every document that still builds emits exactly what it emitted before. | docs/oam/design-gvk.md already states that an unrecognised key is a build error; this moves daemonset toward the documented contract rather than away from it. Gating it to deployment would leave daemonset permanently accepting shapes that do no work. |
An empty key with a non-Exists operator is now an error | New errors only, and only on documents the apiserver would have refused. Upstream states it as a hard “must” (k8s.io/api@v0.36.3 core/v1/types.go:4093). | Nothing appliable is lost. |
tolerations authored as a mapping rather than an array is now an error | New errors only, and output is byte-identical. The whole property was previously discarded without a word — the parser’s []any assertion failed and it returned “absent”, so a mistyped block emitted no tolerations at all and the build succeeded. Its two sibling parsers on the adjacent lines (affinity, topologySpreadConstraints) already rejected the same mistake, so this removes an inconsistency rather than adding a rule. | A silently discarded property is the failure mode this whole projection exists to remove; leaving daemonset on the old behaviour would keep the one parser that swallows a typo. |
An explicit null on a toleration’s key, operator, value or effect now reads as omission | Fewer errors, and output is byte-identical for everything that already built. Such an entry previously failed conversion with must be a string, got <nil>; it now behaves as if the key were absent, which is this package’s null-as-omission convention. Only null is affected — an empty string is unchanged, so operator: "" is still an error rather than a silent default. | The null cannot be filtered before it arrives: withoutExplicitNulls strips only top-level properties, so an author writing a nested null reaches the parser with it intact. ValidateAuthoredProperties (pkg/oam/property_validate_authored.go) does shape-check authored documents, but its reach stops at any path that skips it entirely — a component a lowering rule constructs directly in Go, or a built-in parser’s own field read, this file among them (pkg/oam/README.md’s “Scope” paragraph) — which is why the parser has to answer for itself and why these guards stay. A lowering rule’s emitted null under an optional declared key is now normalised to absence before conversion (same file, validateObjectProperties), so the two paths agree on what a null means rather than one accepting what the other refuses. Leaving daemonset out would keep one kind refusing documents the schema declares valid. |
A value under operator: Exists is now an error | New errors only, and only on documents the apiserver would have refused — upstream’s ValidateTolerations rejects the pair outright, notwithstanding the field doc’s softer “should” (the citation, and its second-hand provenance, are at the check itself in common.go). | Same: nothing appliable is lost. |
A non-empty key that is not a qualified name is now an error | New errors only, and only on documents the apiserver would have refused. ValidateTolerations applies ValidateLabelName to every non-empty key, unconditionally and behind no feature gate. The empty key is untouched — it remains legal and keeps its own rule. | Nothing appliable is lost, and it is the same rule the affinity and topology-key paths already apply to their own label keys; daemonset accepting a malformed key there and not here was an inconsistency, not a policy. |
Under operator: Equal, a value that is not a valid label value is now an error | New errors only, and only on documents the apiserver would have refused. The value is matched against a taint’s value, so upstream runs IsValidLabelValue on it for the Equal arm. Exists and Lt/Gt reach their own value rules first, so this fires on Equal alone. | Same: nothing appliable is lost. |
tolerationSeconds without effect: NoExecute is now an error | New errors, and not only on documents the apiserver would have refused — this row is reason 2 below, not reason 1. Upstream rejects the pair outright, and an omitted effect does not satisfy the rule (it has to be spelled out), but the apiserver never saw the pair: tolerationSeconds was previously dropped along with the rest of the key (first row of this table), so the emitted toleration carried no deadline and applied cleanly. A document hitting this rule may well have built and been accepted by a cluster before. This one was deliberately left unenforced until now, on the stated grounds that the only reachable citation was a second-hand copy carried by a dependency; the comment named a first-hand citation as the condition for adding it, and that condition is now met. | A tolerationSeconds on any other effect is inert at best and refused at apply at worst, which is the failure class this projection exists to remove. Leaving it would also contradict the rule’s own recorded condition. |
Net, stated by cause rather than by a count or a partition. A count here went
stale twice; the replacement then asserted that the classes were exclusive and
that exactly one of them contained previously-accepted documents, and both of
those were wrong too. Three failed summaries of the same table is a sign the
summary wants a different shape, so this one describes what the rows have in
common and makes no claim about which row a document lands in. A daemonset
document stops building for one of these reasons, and one document can hit
several at once:
- It was refused on apply anyway. An empty
keywith a non-Existsoperator, avalueunderExists, a non-qualified non-emptykey, an invalid label value underEqual. Nothing appliable is lost. - Part of it was being discarded unread. An unrecognised member inside an
entry,
tolerationsauthored as a mapping rather than an array, or atolerationSecondsthe parser never looked at — malformed, or well-formed but carrying aneffectother thanNoExecute.
Reason 2 is the one that matters, and it is not confined to documents that were
doing nothing: an entry can carry an unread member alongside tolerations that
worked, and a tolerations block authored as a mapping left the rest of the
workload building and being accepted. So a document rejected under reason 2 may
well have built and been accepted by a cluster before — it simply was not
getting what it asked for, and the apiserver never saw the part that was
dropped. Reading those fields, which is the fix, is what makes them visible.
There is no version of the fix that keeps these documents building and also
emits what they asked for.
Output is byte-identical for everything that still builds, unless it authored a
well-formed tolerationSeconds with effect: NoExecute, in which case the
field it wrote now appears — the defect being fixed, not a regression.
The table is not uniformly a tightening: some
rows go the other way, accepting or emitting what daemonset previously refused
or dropped — a well-formed tolerationSeconds, operator: Lt/Gt, and a nested
explicit null in a toleration entry. Read the middle column per row rather than
assuming a direction. This is pre-release v1alpha1; the change is
taken deliberately rather than hidden behind a deployment-only option whose
removal would depend on unrelated work landing.
One rule applies to both affinity terms and topology-spread constraints:
matchLabelKeys (and mismatchLabelKeys on a pod affinity term) cannot be set
without a labelSelector.
A second rule applies to matchLabelKeys only: none of its keys may already be
constrained by that labelSelector, in either matchLabels or
matchExpressions. mismatchLabelKeys is deliberately not held to it, even
though its upstream field doc reads as a mirror of matchLabelKeys’: upstream’s
validation only ever builds its forbidden-key set from matchLabelKeys, because
a mismatchLabelKeys entry is merged as a NotIn requirement and filtering
further on the same key is a legitimate thing to want. Enforcing the doc’s
wording would refuse a document the API server accepts.
A third rule applies to the entries of both lists, and to a pod affinity term’s
namespaces: every matchLabelKeys / mismatchLabelKeys entry must be a valid
label key (a qualified name), and every namespaces entry a valid DNS-1123
label. These are shape rules the API server enforces on apply, so a document
that fails them was never going to reach the cluster — accepting it here only
moves the failure from build time to admission time. That is the opposite
direction from the mismatchLabelKeys overlap rule declined just above: there,
upstream does not validate, so enforcing would be stricter than the API; here
it does, so accepting is looser. Both errors are worth avoiding and they are
not in tension.
An empty labelSelector: {} is accepted here, unlike on a volume claim’s
selector where launcher refuses it. Upstream distinguishes the two: a null
labelSelector on a PodAffinityTerm matches no pods, while an empty one
matches every pod in scope. Refusing the empty form would make a real API shape
unexpressible.
These three keys live in the deployment handler’s own property map, above the
maps.Copy calls that merge the shared pod-level and DeploymentSpec
fragments, and they must stay there. maps.Copy overwrites the destination, and
worker and webservice each set the four-key affinity shorthand in their
own map and then copy the shared pod-level fragment over it — so moving a raw
affinity into that fragment would silently replace the shorthand on both,
with no fixture moving to reveal it.
Routing traits on a deployment need an explicit Service. expose,
ingress and httproute are accepted on this kind — nothing restricts them —
but they are not self-sufficient here. expose lowers into ingress or
httproute, and both resolve an implicit backend through the component’s own
service port. A deployment has no service port and emits no Service, so an
implicitly-backed route fails the build with a “has no service port” error.
Route to it by naming the target Service on the trait, with serviceName and
servicePort (serviceName requires servicePort; setting servicePort alone
routes to a Service named after the component). That Service must exist
independently — authored as a manifests component, or belonging to another
component in the package. Nothing in this kind creates it.
With automatic NetworkPolicy synthesis, a trait-level serviceName that differs
from the component name is an external backend: a networkPolicy.trafficSources
entry on such a trait lands its allow on the pods of the component that owns the
named Service (on servicePort), not on this component’s. When no component in
the package owns that Service, no allow is synthesized for it and it stays
authored. A serviceName equal to the component name — like servicePort alone
— is treated as this component’s own Service and keeps the allow here. worker
behaves the same way, and so do daemonset and statefulset, which emit no
Service either (go-kure/launcher#690): a statefulset routing to its governing
Service names that authored service, which owns it, so the allow lands on that
Service’s selector.
scaler is available on deployment, as on webservice and worker. An HPA
scales the Deployment past its replica count, and the non-RWX guard below checks
only the component’s effective replica count (authored, or the policy default),
so it cannot account for later HPA scaling. The scaler trait covers
that gap itself: deployment reports its first non-RWX claim to the trait
(NonRWXClaim), and the trait refuses an effective maxReplicas above 1 when
one is present (see “Non-RWX volumes”). statefulset is still excluded: besides
claims referenced from volumes, as on the other kinds, it has
per-pod claims from volumeClaimTemplates, so the question differs.
deployment reports its pod template labels (PodTemplateLabels), so a
same-name sibling group can tell that a service member selects this
Deployment’s own pods, which the group’s component-label policy already
covers (see pkg/oam “Same-name sibling groups”). statefulset, daemonset,
job and cronjob do not have the method, deliberately (go-kure/launcher#794,
item 3): its one reader is that group check, and deployment is the only pod
kind a lowering rule emits into a group (webservice, worker). A rule that
emits another pod kind into a group must add the method to that kind.
| Property | Type | Effect | Compatibility |
|---|---|---|---|
strategy | object | type (required when strategy is authored) is Recreate or RollingUpdate; rollingUpdate may only accompany RollingUpdate, matching ValidateDeploymentStrategy, which marks it Forbidden under Recreate. Omitting strategy entirely writes nothing, leaving apiserver defaulting to supply RollingUpdate with 25%/25%. | additive |
strategy.rollingUpdate.maxUnavailable | int ≥ 0 or percentage string | Same form ValidatePositiveIntOrPercent accepts: a non-negative integer, or a ^[0-9]+%$ string — so +50% is rejected, as upstream rejects it. Capped at 100%, matching the IsNotMoreThan100Percent call upstream makes on this field, and additionally required to be convertible by the rollout controller (see maxSurge). | additive |
strategy.rollingUpdate.maxSurge | int ≥ 0 or percentage string | Same form, not capped at 100%: ValidateRollingUpdateDeployment calls IsNotMoreThan100Percent on maxUnavailable only, so a surge above 100% is a legal Deployment and is accepted here. This is one of two places the Deployment contract differs from the DaemonSet one, which is why the two parsers are separate functions rather than one shared helper. Uncapped is not unbounded: the ^[0-9]+%$ form check accepts any run of digits, so a percentage too long for the rollout controller’s own strconv.Atoi (getIntOrPercentValueSafely, which ResolveFenceposts calls) is rejected here, naming the field, rather than carried verbatim into the object to fail during a rollout with this component out of the message. The boundary is that conversion’s, so nothing upstream accepts is refused here. | additive |
strategy.rollingUpdate (both knobs zero) | — | Rejected: ValidateRollingUpdateDeployment refuses maxUnavailable: 0 together with maxSurge: 0, a pair that can make no progress. It rejects only that pair — unlike the DaemonSet rule, which also rejects both being non-zero. SetDefaults_Deployment guards each field with its own == nil check, so authoring one knob does not suppress the other’s 25% default and a lone authored zero is always legal. | additive |
minReadySeconds | int ≥ 0 | Seconds a new pod must be ready before it counts as available. Must stay below the effective progressDeadlineSeconds, whose own API default is 600 — so minReadySeconds: 600 alone is rejected here, exactly as the apiserver would reject the defaulted pair. | additive |
revisionHistoryLimit | int ≥ 0 | Old ReplicaSets retained. 0 is meaningful (retain none) and distinguishable from unset. | additive |
paused | bool | Pauses rollouts of the Deployment. | additive |
progressDeadlineSeconds | int ≥ 0 | Must be greater than the effective minReadySeconds, the cross-field rule ValidateDeploymentSpec applies. Both halves are compared as effective values, because both have an API default a document may be leaving it to: minReadySeconds defaults to 0 (a non-pointer int32, so it has no unset state) and progressDeadlineSeconds defaults to 600. So the rule fires in both directions — progressDeadlineSeconds: 0 alone is rejected against the defaulted 0, and minReadySeconds: 600 alone is rejected against the defaulted 600 — and the error names, for each half, whether the value was authored or defaulted, since either can be a field the document never mentions. | additive |
selector, template | — | Not authorable, and rejected with a message saying so rather than silently ignored. The selector is builder-managed (app: <component>) and immutable once the object exists; the pod template is projected from the component’s own container and pod-level properties. | additive |
any of the five DeploymentSpec properties above, authored as null (on deployment, webservice and worker alike); and, on deployment only, any optional property of the kind | — | Read as omission, not as a present-but-wrong type — including a typed nil, which is what a Go-constructed lowering rule produces when it assigns a nil map into an any. pkg/oam’s property validator already treats a null under an optional property as absent, so without this a component could satisfy the published schema and then fail during handler conversion. parseDeploymentSpec strips nulls itself, so the five fields above behave this way on webservice and worker too. The wider guarantee is the deployment kind’s alone: there it covers the kind’s whole top-level surface, so replicas: null, workingDir: null, env: null and the rest all read as unauthored and replicas: null takes the default 1. Since go-kure/launcher#394 this is no longer specific to deployment for a property read through one of the shared field helpers in common.go, which test presence with authoredValue, nor for the six parsers that used to read their own property with a bare map lookup rather than a helper — envFrom, probes, lifecycle, securityContext, volumes, accessModes — which were converted to the same primitive. Since go-kure/launcher#570 it also covers the top-level keys that were still answered with a bare lookup and then type-checked — cronjob’s timeZone, successfulJobsHistoryLimit and failedJobsHistoryLimit; completionMode on job and cronjob; passthrough’s clusterScoped; inline and url on crd and manifests; and manifests’ scopeOverrides — so a null there, typed or untyped, now reads as omission instead of a wrong type. That mattered because ValidateAuthoredProperties does not strip a top-level null, so kurel build used to reach the refusal. One deliberate exception, and it is not an oversight: the postStart/preStop handler keys (httpGet, exec, sleep, tcpSocket) still answer presence with a bare lookup, because there the rule being enforced is presence — lifecycle: {preStop: {tcpSocket: null}} must stay a refusal, since reading it as absence would let an authored-but-empty tcpSocket vanish and a valid sibling handler win silently, which is the exact silent-drop the surrounding paragraph rejects. The exception covers a probe’s handler keys (httpGet, tcpSocket, exec, grpc) as well: countProbeHandlers reads them the same way, for the same reason, so a null one is refused too. Absence and authored-null are genuinely different documents for those handler keys. A null on a field that is required once its parent is authored (strategy.type) surfaces as the requiredness error, not a type error; selector/template are not optional properties, so naming either as null still earns the refusal above. It now reaches nested keys too — securityContext: {runAsUser: null} builds as if runAsUser were omitted. That fix deliberately sits inside each nested parser, after its unknown-key rejection, rather than in a recursive pre-strip over the property map: a recursive strip would remove securityContext: {bogusKey: null} before parseSecurityContext’s own rejection ever saw it, turning a named refusal into silence — trading one silent-drop bug for another. So a recognized nested key read through the helpers treats a null as absence, and an unrecognized one is still named and refused; both sides of that securityContext boundary are pinned in deployment_nested_null_test.go. The two nested map readers that used to read each entry raw now skip a null entry too, as stringMapStrict already did: parseResourceList (a resources.requests/limits quantity, where property validation deletes a null first only under the declared cpu and memory keys) and parseLabelMap (a nodeSelector, service selector or matchLabels value). As with securityContext, the skip comes after the key check: an invalid resource name or label key is still refused whatever its value, and a wrongly typed entry is still refused. A map left empty by the skip meets the empty-map refusal where one exists — service’s selector: {app: null} is refused as selector: {} is, rather than emitting a selector-less Service. | additive |
Every field above is presence-gated: a document that authors none of them produces the same object the builder produced before, so the apiserver’s own defaults still apply rather than being frozen into the manifest.
An existing webservice or worker document that authors none of the five
builds byte-identically, and the two rules that look like new restrictions are
reachable only through them: a strategy contradicting the non-RWX guard, and
the selector/template refusals. The one visible change to a document
authoring none of them is the wording of the non-RWX replica refusal, now the
shared message (“a non-RWX PVC allows at most one replica”) rather than each
kind’s own; the documents it refuses are exactly the ones it refused before.
A document that was authoring one of the five keys is a different case, and
it is not covered by “additive”. These kinds did not previously refuse those
keys as unknown — they ignored them. A handler reads the properties it knows
and drops the rest, and at the time this paragraph was first written the
authored path had no shape check of its own: validateProperties runs on
emitted elements only (pkg/oam/property_validate.go), and validate.go
checks type names and identity but not property shape. So strategy on a
webservice was accepted and silently discarded before go-kure/launcher#341,
and is honoured after it. Such a document kept building but compiled to a
different Deployment, and one authoring a malformed value now fails the build
where it previously succeeded.
Under the additive test in docs/oam/design-gvk.md — every previously valid
document stays valid and compiles to the same output — that was not
additive. The gap was not this change’s: it applied to every property ever
added to an existing kind, because the authored path did not enforce the
Parser Strictness that section promises. docs/oam/design-gvk.md § Parser
Strictness states that an unrecognised key is a build error, which was true of
the document envelope and not of a component’s properties map. Since
go-kure/launcher#408 (closed), it is true of both: kurel build now calls
ValidateAuthoredProperties (pkg/cmd/kurel/build.go:149, after parameter
resolution so a package-mode ${...} placeholder is not checked against its
own bare-string type), which walks declared keys and shapes on the authored
document itself. An authored strategy malformed enough to fail that check now
fails there, before the handler ever sees it.
replicas is validated the same way on every kind that reads it —
deployment, webservice, worker, statefulset and postgresql share one
checked reading (parseReplicas, common.go). A value that is not an integer
is an error naming its type (replicas: must be an integer, got string), and a
negative is an error naming the value (replicas: must be >= 0, got -1).
Absent or null takes the default 1. replicas: 0 is accepted everywhere.
An authored count, including 0 and 1, wins over a policy DefaultReplicas;
only an absent or null one takes it.
Before go-kure/launcher#393 only the deployment handler did this. The
other handlers fell back to the default when the value would not convert, and
carried replicas: -1 through to an object the apiserver then refused
(ValidateDeploymentSpec and ValidateStatefulSetSpec run
ValidateNonnegativeField on it). What that changes in practice differs by
entry point:
kurel buildalready refused a non-integer on every kind through the authored-property check (see above). The change it sees is the negative count, which is now a build error on those kinds too.- A library caller that runs the handlers without
ValidateAuthoredPropertiesused to get one replica forreplicas: "3"without any error. It now gets the handler’s own error.
Every integer property accepts every Go integer kind. The shared readers
(toInt32/toInt64, parsePort, the history limits, quantity and whole-number
rendering in common.go) read through oam.IntegerValue. So a uint16 port or an
int8 history limit from a lowering rule or a Go caller reads like the int a YAML
literal decodes to. Each reader then checks the value against its own target (a port
is 1–65535, a count fits int32) and refuses it with an error rather than truncating
or wrapping (go-kure/launcher#525). That includes the main and sidecar container
ports. (daemonset/statefulset read port: 0 as “no port” until they
dropped port, go-kure/launcher#690.)
Non-RWX volumes. A ReadWriteOnce (or ReadWriteOncePod) claim cannot be
held by an outgoing and an incoming pod at once, so the handler allows at
most one replica (replicas: 0 is a deliberate scale-to-zero and is accepted
rather than coerced to 1) and forces strategy.type: Recreate. The
scale-to-zero exemption covers the replica count only: the strategy is still
forced, and an authored RollingUpdate still refused, at zero replicas —
scale-to-zero is a state a later edit can leave by changing one number, and a
RollingUpdate that survived the guard while at zero would be wrong the moment
the first pod starts. Kubernetes rejects neither
combination — the second pod simply hangs unschedulable or stuck attaching — so
this is a build-time guard, not a mirror of an apiserver rule. An authored
strategy.type: RollingUpdate in that situation is rejected rather than
overwritten, so a document never ships a strategy its author did not write.
That refusal reaches webservice and worker only now that they publish
strategy at all; before go-kure/launcher#341 those two substituted Recreate
silently, because there was no authored value to contradict. deployment,
webservice and worker all run the one applyNonRWXConstraint.
That guard checks the component’s effective replica count, but a scaler
trait’s HPA scales the same Deployment up to maxReplicas, which the guard
cannot see. So on deployment, webservice and
worker the trait is held to the same limit: with a non-RWX claim attached, an effective
maxReplicas above 1 fails the build with an error naming the scaler trait
and the claim’s volume. “Effective” means after an EnvironmentPolicy
scalerMaxReplicas default is applied. maxReplicas: 1 still builds. On
webservice and worker, a document that combined the two used to build and
then left pods 2 and up unschedulable or stuck attaching; it is now refused
as a pre-GA format change (docs/oam/design-gvk.md § Document-Format
Lifecycle). On
deployment the combination was never accepted, because scaler was not
admitted there: the trait is newly admitted with this safeguard in place.
The guard reads a claim’s whole access-mode set, not each mode on its own.
accessModes requests a volume supporting every mode listed, so
[ReadWriteOnce, ReadWriteMany] binds a volume many pods can mount read-write
and is left unconstrained — replicas above one and a rolling update are both
fine. [ReadWriteOnce] or [ReadWriteOnce, ReadOnlyMany] still constrain the
workload: neither makes the read-write mount shareable. The test is per claim,
so one shareable claim does not excuse a second ReadWriteOnce-only one on the
same component. ReadWriteOncePod never appears beside another mode — the API
forbids the combination and the parser rejects it — so it is always constraining.
That guard is one helper shared with webservice and worker, so reading the
whole set changes what those two kinds build for a pre-existing document
whose claim declares [ReadWriteOnce, ReadWriteMany]: replicas: 2 used to be
a build error and now succeeds, and a single-replica document used to ship
strategy.type: Recreate and now ships no strategy at all, leaving the
apiserver’s RollingUpdate default to apply. This is the one place in this
change where an existing document’s output moves. It is deliberate and
one-directional — the correction only accepts documents that were being
refused, and only stops forcing a strategy the author never wrote — and it is
not scoped to the new kind, because scoping it would leave webservice and
worker refusing volumes Kubernetes shares happily. Both halves are pinned per
kind (rwx_shared_helper_test.go).
Policy defaults & enforcement ordering
A container’s effective cpu/memory requests and limits are assembled in three tiers, in precedence order:
- Authored — whatever the component’s
resourcesproperty set explicitly. - Policy default —
ApplyPolicyfills any request/limit the author left unset fromoam.Policy.DefaultCPURequest()/DefaultMemoryRequest()/DefaultCPULimit()/DefaultMemoryLimit()(enforce.go’sapplyDefaultQuantity). A negative policy default fails the build, as a negative authored quantity does, since Kubernetes would refuse it at apply; zero is accepted. - Intrinsic handler default —
buildResourceRequirements(common.go) fills anything still unset atGenerate()time: 100m CPU request, 128Mi memory request, and a memory limit mirroring the (possibly just-defaulted) memory request.
oam.Policy.MaxCPU()/MaxMemory() are enforced against the effective
value — what Generate() will actually emit, after all three tiers — not
just the authored/policy-defaulted resources field. Prior to go-kure/launcher#251
the enforcement check ran before the intrinsic tier was computed, so an
application that omitted spec.resources entirely could ship a Deployment
whose 100m CPU / 128Mi memory intrinsic defaults exceeded the enforced
maximum. enforceMaxResources (enforce.go) closes this by calling the same
buildResourceRequirements the generator calls, enforcing against its
result, and discarding it — buildResourceRequirements deep-copies its
maps, so this is read-only and generated output is unchanged. When a value
came from the intrinsic tier specifically (absent from the authored/policy-
defaulted value, present only after the intrinsic fallback), the error names
it as a “generated default” so the mismatch isn’t mysterious.
This three-tier effective-value enforcement applies to the seven kind
components that call buildResourceRequirements on their main container
(webservice, worker, deployment, cronjob, job, statefulset,
daemonset).
postgresql is exempt: its lowering rule copies every entry of
c.Resources straight onto the Cluster spec (cnpgResourceList) and never calls
buildResourceRequirements, so it has no intrinsic tier for its existing
direct-form checks to diverge from.
Init containers and sidecars receive the same intrinsic defaults
(common.go’s buildResourceRequirements call sites at the init-container
and sidecar builders), and ApplyPolicy enforces them too (go-kure/launcher#312):
each entry’s resources, image registry, securityContext.privileged, and
securityContext.capabilities.add are checked against the same policy
methods the main container uses, via the shared enforceExtraContainer
helper (enforce.go). All seven kind components enforce their
initContainers; only webservice, worker, deployment, statefulset
and, since go-kure/launcher#790, daemonset have a
sidecars schema key at all (cronjob/job have no sidecars support,
per “Per-type highlights” below) so only those five enforce a sidecars
loop. Errors name the authored list position and container, e.g.
initContainers[0] "init": image "docker.io/x/y:v1" is not from an allowed registry [...].
The twelve JobSpec-level properties (see “Per-type highlights” below) are parsed
and applied by a dedicated JobSpecConfig/parseJobSpec/applyJobSpec
(common.go), factored out separately from the fields above because
batchv1.CronJob.Spec.JobTemplate.Spec and a bare batchv1.Job.Spec are the
same batchv1.JobSpec type. cronjob projects them onto
spec.jobTemplate.spec and job (go-kure/launcher#344) onto its own
spec; both publish the identical key set, pinned by
TestJobSpecSchemaMatchesParser against jobSpecPropertyKeys.
One JobSpec field is deliberately not shared: suspend. batchv1 carries
two distinct Suspend fields — CronJobSpec.Suspend (stop starting new
executions of the schedule) and JobSpec.Suspend (create the job with no pods)
— and cronjob already published suspend for the first of them. Putting a
suspend in the shared fragment would write one authored value into two
different API fields, and would silently overwrite cronjob’s own schema entry,
since each handler merges the shared fragment with maps.Copy after its own
literal map. So job publishes and parses its own JobSpec-level suspend
instead, and cronjob’s stays CronJobSpec-level with no key for the job-template
one. Both halves are pinned: TestSchemaJobSpec_HasNoSuspendKey on the schema,
TestCronjobHandler_SuspendWritesCronJobSpecOnly and
TestJobHandler_SuspendWritesJobSpec on the generated objects.
applyJobSpec copies every value it writes rather than assigning the config’s
own pointers (copyPtr for the scalars, DeepCopy for successPolicy and
podFailurePolicy, which each own a slice of rules). A handler config outlives one Generate call and
Generate may run more than once, so an aliased pointer would leave a mutable
path from the generated object back into the config — a writer through the
object would change what the next Generate emits.
Policy refusal classes
Every refusal by the environment policy this package raises is an oam.PolicyRefusal with
a class, which a consumer reads from oam.ViolationError.Class instead of matching text
(go-kure/launcher#849). The classes, what each covers and what stays unclassified are in
pkg/oam/README.md, “Refusal classes”. What is particular to this package:
- The class does not depend on the path. A privileged container is
oam.RefusalPrivilegedon a workload kind’s ownApplyPolicy, on an operator CR’s pod template, on an object a chart renders, and on apassthroughormanifestsobject, at the transform and at generation: one check raises it (enforce.go,enforcePodTemplatePolicy,enforceRenderedObjectPolicy). - cpu and memory are one class, storage another. The comparison with a maximum is
shared and its text is the same for every resource, so the class comes from the caller:
oam.RefusalResourceMaximumfor a cpu or memory request or limit (a container’s, a pod’s, the block a CloudNativePG kind, analertmanager, athanosruleror aprometheuscarries, and the pod template of an ACME HTTP01 solver on anissueror aclusterissuer),oam.RefusalStorageMaximumfor a claim’s request, a claim template’s, a generic ephemeral volume’s, a PersistentVolume’s capacity and acnpg-clustervolume, the1Gifallback ofpostgresqlincluded, which its post-policy step holds to the maximum.oam.RefusalReplicaMaximumcoversreplicas, a HorizontalPodAutoscaler’smaxReplicasandcnpg-cluster’sinstances. - Secret material in the document is
oam.RefusalExplicitSecret: asecretcomponent, a Secret thatpassthroughor amanifestssource carries,helmtemplate’ssecretValues, acertificate’s keystore password (keystores.jks.password,keystores.pkcs12.password), and in asecretstoreor aclustersecretstorea credential written as avalueand the data of thefakeprovider. - A source host is
oam.RefusalRegistry, as an image’s registry is: theurlof amanifestsorcrdsource, ahelmtemplatechart source, the Flux source kinds, anoci://url that does not name its registry, and abucketendpoint on Amazon S3. - An object that cannot be read is
oam.RefusalUnreadableObjecton the three paths that take an object written elsewhere (template delivery,passthrough,manifests): the refusals under “What cannot be read is refused, not passed” in thepassthroughandmanifestsentries. - Unclassified, though it is the component’s violation: the rules that hold with or
without a policy and are checked at the policy step (
ephemeralContainersand the image rule on an object written elsewhere, an undeclared field on a workload or a claim a chart renders), a policy default or maximum that does not parse, and a chart that does not render or whose render does not decode (anitemsarray on a kind that is no list, for one). - A redirect of a
manifestsurlsource to a host outside the allowed registries is a fetch failure, with the error a fetch failure has and nooam.ViolationError; that error holds theoam.PolicyRefusalof classoam.RefusalRegistry, whicherrors.Asreaches.
A new policy check in this package builds its refusal with oam.NewPolicyRefusal and a class
constant; TestBuiltinPolicyRefusalsCarryAClass (pkg/oam) fails on one that does not.
Pod view of a monitoring workload object (MonitoringPodSpec)
An object from which the Prometheus operator builds pods, the Alertmanager, Prometheus and
ThanosRuler of the alertmanager, prometheus and thanosruler kinds, has no pod template:
its pod fields sit under spec in the operator’s own type, so a consumer that scans the pods of
the rendered output finds none to scan (go-kure/launcher#974). MonitoringPodSpec(obj) returns them as a corev1.PodSpec, with the
prefix its paths sit under in the object (spec) and whether the object is of such a kind:
- What it holds: the fields the kind’s policy checks read, from the same mapping: the listed
containersandinitContainers,volumes,securityContext,hostNetworkwhere the spec has it (aThanosRuler’s has none),dnsPolicy, and thenameserversofdnsConfig. A listed container named for one the operator generates is a patch the operator merges into that container, and is in the view as written. - What it does not hold: the containers the operator generates itself. Their images,
arguments and security context are the operator’s, and launcher holds only what the author
wrote. The
searchesandoptionsofdnsConfigare not in the view either.Kind Generated containers Generated init containers alertmanageralertmanager,config-reloaderinit-config-reloaderprometheusprometheus,config-reloader, andthanos-sidecarwherethanosis setinit-config-reloaderthanosrulerthanos-ruler,config-reloadernone - A copy: a change to the view does not reach the object.
- Typed objects only: an unstructured monitoring object is converted to its typed object
first. Any other object, a nil one included, gives
nil, "", false.
Each monitoring workload kind adds its case from its own monitoringWorkload mapping;
TestMonitoringPodSpec_CoversEveryRegisteredWorkloadKind (pkg/oam/builtin/registry) fails on a
registered kind of the operator’s API whose spec lists containers and that has no case.
Per-type highlights
Wrong-type handling for the optional top-level properties go-kure/launcher#405
moved onto the presence-reporting helpers: port on webservice, daemonset
and statefulset (the latter two have since dropped it, go-kure/launcher#690;
parseInt32Field; on webservice the Endpoints declaration runs the
rule’s whole parse, so a wrong type is not declared as an endpoint on port 80
either, and neither is a component without an image; what the rule’s
lowering refuses after the parse is one of the four things an endpoint entry
does not see, pkg/oam, “Name roles and the Naming hook”), topologySpread on webservice and
worker and prune on oci (parseBoolField), serviceName on
statefulset and path, interval and targetNamespace on oci
(parseStringField, so an authored "" still reads as absent), and
restartPolicy on cronjob (read raw, as job reads it, so "" still
reaches the enum refusal). A present value of the wrong type is rejected by
name (topologySpread: must be a boolean, got string) and a null reads as
absence. Before, each fell back to its default as though the key had been
omitted — topologySpread: "false" kept the spread constraints and
prune: "false" pruned — for a handler called directly; schema validation
already refused these shapes in an authored document. One authored shape does
change: port is declared integer with no maximum, so an integer outside the
int32 range (port: 5000000000) passed validation and built as though no port
were authored (80 on webservice, none on daemonset/statefulset); it is
now refused by value, not by type (port: must be an integer within int32 range, got 5000000000). parseInt32Field words that case apart from a wrong
type for every field it reads, so an out-of-range replicas or
minReadySeconds gets the same range message. postgresql’s other top-level
reads (storageSize, backup, pooler, …) were converted separately, in
go-kure/launcher#512 (see the postgresql entry below).
apiservice, validatingwebhookconfiguration, mutatingwebhookconfiguration (go-kure/launcher#943) are the kind-named projections of three cluster-scoped objects: an
apiregistration.k8s.io/v1APIService and anadmissionregistration.k8s.io/v1ValidatingWebhookConfiguration and MutatingWebhookConfiguration. Each emits that one object, named after the component unlessobjectNamenames it, with no namespace, holding exactly what was authored, onpolicyFreeKind. No dimension of the environment policy reads them; its object kind rules (oam.ObjectKindPolicy) gate them as every emitted object, so a policy that forbids their group, their kind or cluster-scoped objects refuses them. What they do once applied is out of the build’s reach (see “Kind inventory”, “Cluster-wide admission and API registration”).apiservice decodes
APIServiceSpecstrictly. From the markers,groupPriorityMinimumandversionPriorityare required, which the type writes unauthored. By hand, from the API server’s validation:versionmust be written,grouptoo (the API exemptsv1, the core API, whose APIService is namedv1.; that is no DNS-1123 subdomain, so no component name orobjectNamecan give it, and the kind requires the group), and an authoredserviceitsnamespaceandname. The API requires the object to be named<version>.<group>, so the kind refuses any other name at generation (the APIService is named "metrics", and the API requires the name "v1beta1.metrics.example.com" (<version>.<group>): name the component so, or set objectName). A naming hook that renames the object is held to the same rule. The Service is not resolved at build. The priorities' ranges (1 to 20000, 1 to 1000), the port, and howcaBundleandinsecureSkipTLSVerifycombine are left to the API server.caBundleis base64, as the API’s JSON writes bytes.validatingwebhookconfiguration and mutatingwebhookconfiguration have no spec: their one property is the object’s
webhooks, strictly decoded, andkind,apiVersionandmetadataare refused. From the markers, a webhook’sname,clientConfig,sideEffectsandadmissionReviewVersions, a Service’snamespaceandname, and a match condition’snameandexpressionmust be written. By hand, from the API server’s validation at Kubernetes v1.37.1:clientConfigholds exactly one ofurlandservice(webhooks[0].clientConfig: exactly one of url and service is required …),admissionReviewVersionsholds at least one entry, and a rule names at least one operation, API group, API version and resource (webhooks[0].rules[0].resources: required …). Each webhook’snamespaceSelectorandobjectSelectorare held to the shared check (see “A label selector’s match expressions”). Not checked here: the form of a webhook’s name and its uniqueness, the values offailurePolicy,matchPolicy,sideEffects,reinvocationPolicyand a rule’s operations and scope, the timeout’s range, a URL’s form, the admission review versions the API server recognises, and how wildcards combine in a rule. A match condition’s CEL expression is compiled by the API server, not at build. Neither the URL nor the Service is resolved at build. A mutating webhook changes objects after the build, so what the build checked need not hold of what is stored. An authoredwebhooks: []emits a configuration with no webhook.fluxcd-alert, fluxcd-provider, fluxcd-receiver, imagepolicy, imagerepository, imageupdateautomation, artifactgenerator, resourcesetinputprovider (go-kure/launcher#790) are the kind-named projections of objects of the Flux APIs beside the sources, the HelmRelease and the Kustomization: a notification.toolkit.fluxcd.io/v1beta3 Alert and Provider, a notification.toolkit.fluxcd.io/v1 Receiver, an image.toolkit.fluxcd.io/v1 ImagePolicy, ImageRepository and ImageUpdateAutomation, a source.extensions.fluxcd.io/v1beta1 ArtifactGenerator, and a fluxcd.controlplane.io/v1 ResourceSetInputProvider of the Flux Operator’s API. Each is built on
policyFreeKind(see the storageclass entry) with the Flux namespace, the check of its durations and, where a field names a host the environment policy holds or holds a credential, a check under that policy (fluxKind), and emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default. The type isfluxcd-alertrather thanalert, which beside aprometheusrulewould read as an alerting rule.Nothing gates what a Flux object reaches outside its namespace, or the identity it acts under. A Flux object may name objects of another namespace, and some may act under an account that is not their controller’s. Launcher writes such a field as authored and refuses none, under a nil policy and a strict one alike: the environment policy has no rule for the namespace or the account a Flux object names, and
ApplyPolicychecks neither here, as it checks neither onhelmrelease(itschart.spec.sourceRef.namespace,chartRef.namespace,serviceAccountNameandkubeConfig) and onfluxcd-kustomization(itssourceRef.namespace,serviceAccountNameandkubeConfig). Whoever may author a component may author these fields, and a consumer that restricts them restricts the component types it registers. Assumption, not read here: a cluster’s Flux controllers can be started so that they refuse a reference into another namespace. Launcher reads no such setting, and no build depends on it.In the Flux namespace these objects share a namespace with every other application’s Flux objects. When a Flux namespace is set, the objects of this group land there (Namespace, below), and so do those of every other application built with that Flux namespace. A reference without a namespace, and a selector over “the object’s namespace”, then reach them: an Alert’s
providerRefmay name another application’s Provider, a Receiver’sresourcesanother application’s Flux objects, an ImagePolicy’simageRepositoryRefanother application’s ImageRepository, the source of an ImageUpdateAutomation or of an ArtifactGenerator another application’s source, a ResourceSetInputProvider’s selector without anamespaceanother application’s ExternalArtifacts, and an ImageUpdateAutomation without apolicySelectortakes every ImagePolicy there. An ImageRepository there is in the namespace of every other application’s ImagePolicy, so a reference to it is not the cross-namespace one the API saysaccessFromis for. Nothing gates this either, and nonamespacehas to be written for it.The fields, per kind:
fluxcd-alert:eventSources[].namespacenames the namespace of the objects whose events are sent, and a source’sname: "*"takes every object of its kind there, or, withmatchLabels, every one that carries those labels. The events go to the ProviderproviderRefnames, so an Alert sends what happens to another namespace’s Flux objects to a receiver its author chose.providerRefholds a name and no namespace: the Provider is one of the namespace the Alert lands in. An Alert names no account.fluxcd-provider: the object makes the notification controller send the events of the Alerts that name it toaddress, which the API calls “the endpoint, in a generic sense, to where alerts are sent” (for some types “a project ID or a namespace”), throughproxyor the proxy the Secret ofproxySecretRefholds. Both are hosts outside the cluster unless the receiver runs in it, and launcher writes both as authored.serviceAccountNamenames the account the controller authenticates as: in the API’s words “the Kubernetes ServiceAccount used to authenticate with cloud provider services through workload identity”, for the typesazureeventhub,azuredevopsandgooglepubsub, and only with the controller’sObjectLevelWorkloadIdentityfeature gate. The field holds a name and no namespace. Assumption, not read here: the controller takes the account from the namespace of the Provider, which in the Flux namespace holds the accounts of every application’s Flux objects. Launcher writes it as authored and refuses none.fluxcd-receiver: a Receiver opens an inbound path on the notification controller. A request there that the Receiver validates, by the procedure itstypenames (“the validation procedure and payload deserialization”), makes the controller reconcile the objectsresourcesnames.resources[].namespacenames the namespace of the objects a webhook makes the controller reconcile, and a resource’sname: "*"takes every object of its kind there, or, withmatchLabels, every one that carries those labels. So a Receiver lets the sender of a webhook trigger the reconciliation of another namespace’s Flux objects. The token a request is validated with is in the SecretsecretRefnames, one of the namespace the Receiver lands in; ageneric-oidcReceiver validates a request by the OIDC issuers ofoidcProvidersinstead.oidcProviders[].issuerURLnames a host outside the cluster, which the controller discovers the issuer at; launcher writes it as authored and holds its host to no policy; a user or a password in it is refused (Policy, below). A Receiver names no account.imagepolicy:imageRepositoryRef.namespacenames the namespace of the ImageRepository whose scanned tags the policy selects from, so an ImagePolicy may name the ImageRepository of another namespace and read the tags it scanned. The API documents, on the ImageRepository, anaccessFromlist “for allowing cross-namespace references to the ImageRepository object based on the caller’s namespace labels”; launcher reads no ImageRepository to see whether the one named allows it. An ImagePolicy names no account.imagerepository: the object makes its controller scan the tags ofimageat the registry that name holds, which is a host outside the cluster unless the registry runs in it.accessFromopens the ImageRepository to other namespaces: the API calls it “an ACL for allowing cross-namespace references to the ImageRepository object based on the caller’s namespace labels”, itsnamespaceSelectorsare joined by OR, and “an empty map of MatchLabels matches all namespaces in a cluster”, soaccessFrom: {namespaceSelectors: [{}]}opens the scanned tags to an ImagePolicy of any namespace.serviceAccountNamenames the account the scan authenticates as: in the API’s words “the Kubernetes ServiceAccount used to authenticate the image pull if the service account has attached pull secrets”. The field holds a name and no namespace. Assumption, not read here: the controller takes the account from the namespace of the ImageRepository, which in the Flux namespace holds the accounts of every application’s Flux objects.insecure: true“allows connecting to a non-TLS HTTP container registry”. The proxy the registry is reached through is not in the object:proxySecretRefnames a Secret that holds its configuration, and no build sees that host. Launcher writes each of the four as authored and refuses none.imageupdateautomation:sourceRef.namespacenames the namespace of the GitRepository that, in the API’s words, gives “access details to a git repository”. An ImageUpdateAutomation makes its controller commit to that repository and push, to the branch the GitRepository orgit.checkout.refnames, or togit.push.branchandgit.push.refspec; so one that names another namespace’s GitRepository writes to a repository with the access details that GitRepository gives. Nothing holds the branch or the refspec either. It names no account. The ImagePolicies whose selections it applies are those of the namespace it lands in (policySelectornarrows them; it names no other namespace), and the Secret ofgit.commit.signingKeyis one of that namespace too. In the Flux namespace, an ImageUpdateAutomation without apolicySelectorselects every ImagePolicy there, those of other applications included (the API: “By default includes all policies in namespace”), and the automation then commits their selections through its own GitRepository.artifactgenerator:sources[].namespacenames the namespace of a Flux source (a Bucket, GitRepository, OCIRepository, HelmChart or ExternalArtifact); left out, the API takes “the same namespace as the ArtifactGenerator”. An ArtifactGenerator copies files out of the sources it names into the artifacts it generates, which the API describes as ExternalArtifacts, so one that names another namespace’s source republishes that source’s content as an artifact its author named. It names no account.resourcesetinputprovider: the object makes the Flux Operator ask the provider itstypenames for input sets, which, in the API’s words, “populate the inputs” of a ResourceSet; the answer decides what that ResourceSet deploys. Every type butStaticandExternalArtifactasks the host ofurl(Policy, below), with the credentials of the SecretsecretRefnames; anExternalServiceprovider sends that Secret’s token or user and password to it. AnExternalArtifactprovider reads ExternalArtifacts of the cluster instead:selectors[].namespacenames the namespace it lists them in, andnamespace: "*"lists them “across all namespaces”. Left out, the namespace is the provider’s own.serviceAccountNamenames the account the operator acts as: in the API’s words the ServiceAccount “used for authentication with AWS, Azure or GCP services through workload identity federation features”, taken, without one, from “the ServiceAccount of the operator”. For anExternalArtifactprovider, the list is made with the operator’s own client unless an account is set; with one, the operator lists as that account, of the namespace the object lands in. Read from the controller of the linked module, not from the type: an operator started with a default account lists as that account where none is set; launcher reads no operator setting, and the installed operator may differ from the linked one. Launcher writes the selectors and the account as authored and refuses none.
Authored. The properties are the top-level json fields of the spec type, decoded strictly at every depth: an unknown key is refused wherever it sits (a source, a reference, a policy).
fluxcd-alert(AlertSpec):providerRef,eventSources,eventSeverity,inclusionList,exclusionList,eventMetadata,summary(deprecated by the API foreventMetadata) andsuspend.fluxcd-provider(ProviderSpec):type,interval(in the API’s words “Deprecated and not used in v1beta3”),channel,username,address,timeout,proxy(deprecated by the API forproxySecretRef),proxySecretRef,secretRef,certSecretRef(each aname),serviceAccountName,suspendandcommitStatusExpr.commitStatusExpris a CEL expression the controller evaluates to the message of a commit status; launcher writes it as authored and does not parse it.fluxcd-receiver(ReceiverSpec):type,interval,events,resources(each anapiVersion,kind,name,namespace,matchLabelsandfilter),resourceFilter,secretRef(aname),oidcProviders(each anissuerURL,audience,variablesandvalidations) andsuspend.resourceFilter, a resource’sfilter, and theexpressionof an OIDC variable and of an OIDC validation are CEL expressions the controller evaluates; launcher writes them as authored and does not parse them.imagepolicy(ImagePolicySpec):imageRepositoryRef,policy(semver,alphabetical,numerical),filterTags,digestReflectionPolicy,intervalandsuspend. An authoredfilterTagsis written with both itspatternand itsextract, the one left out as the empty string: the Go type omits neither.imagerepository(ImageRepositorySpec):image,interval,timeout,secretRef,proxySecretRef,certSecretRef(each aname),serviceAccountName,suspend,accessFrom(namespaceSelectors, each amatchLabels),exclusionList,providerandinsecure.imagenames a repository, such asghcr.io/org/app; launcher does not parse it beyond its registry (Policy, below), and an entry ofexclusionListis a regular expression it does not compile.imageupdateautomation(ImageUpdateAutomationSpec):sourceRef,git(checkout,commit,push),interval,policySelector,updateandsuspend.git.commit.messageTemplateis a template the controller renders; launcher writes it as authored and does not parse it.artifactgenerator(ArtifactGeneratorSpec):commonMetadata,sources,pathPatternandartifacts(each aname,revision,originRevisionandcopy; a copy afrom,to,excludeandstrategy).commonMetadatais not the object’s own metadata: itslabelsandannotationsare written into the spec, where the API says they are “applied to all resources”, the ExternalArtifacts it generates; the ArtifactGenerator’s own labels and annotations are thelabelsandannotationsproperties. At generation they are held to the reserved metadata keys (pkg/oamREADME, “Reserved metadata keys”), and not to the component label’s value: an ExternalArtifact is no pod. Launcher writes nothing there. The aliases, the@<alias>/…paths and the{capture}placeholders are written as authored: launcher resolves none of them.resourcesetinputprovider(ResourceSetInputProviderSpec):type,url,serviceAccountName,secretRef,certSecretRef(each aname),insecure,defaultValues,filter(includeBranch,excludeBranch,includeTag,excludeTag,includeEnvironment,excludeEnvironment,labels,limitandsemver),skip(labels),schedule(each acron,timeZoneandwindow) andselectors(each aname,namespace,matchLabelsandmatchExpressions).defaultValuesis a free map, written as authored; the regular expressions offilter, itssemverrange and a schedule’scronare written as authored and not parsed.- No default is filled. The API’s own (
eventSeverity: info,digestReflectionPolicy: Never, a policy’sorder: asc,update: {strategy: Setters}, an ImageRepository’sprovider: genericand itsexclusionListof^.*\.sig$, a Receiver’sinterval: 10m, a ResourceSetInputProvider’sfilter.limit: 100and a schedule’stimeZone: UTC) is applied by the API server to what the object leaves out; thetimeoutof an ImageRepository left out is, in the API’s words, the “‘Interval’ duration”, and theaudienceof a Receiver’s OIDC providernotification-controller, which are the controller’s to apply. A schedule’swindowleft out is written0s, the API’s own default. One default cannot apply:sourceRef.kind, which the API defaults toGitRepositoryand the Go type writes empty when it is left out, an empty value being a value. It is required instead (below).eventSeverity,digestReflectionPolicy, a policy’sorder,update.strategy, an ImageRepository’sproviderand a schedule’stimeZoneare strings the type omits when empty, so an authored""on one would be left out and defaulted: it is refused (eventSeverity: "" cannot be carried by the Flux API types (…)). A ResourceSetInputProvider’sfilter.limit: 0is omitted the same way, and the API server would then apply its default 100, so it is refused too (filter.limit: 0 cannot be carried by the Flux Operator API types (…)).TestFluxKinds_DefaultedZerosholds each kind’s list of such fields to the numbers, booleans and strings that are omitted when zero and that the API defaults to something else. An authored emptyexclusionList([]) is not refused, and it does not mean that nothing is excluded: the type omits an empty list, so the object gets the API’s default list (^.*\.sig$), and the controller also reads an empty list as that default. - A duration is held to the pattern its field declares, as on the Flux
kinds below (go-kure/launcher#601): unsigned, in the units
ms,s,mandh. A value outside it is refused (imagepolicy: interval "-5m" is invalid: must be a Flux duration (…)), and so is one below a millisecond, which would be written in a unit the pattern does not take (0.5msas500µs). It is written as Go formats it (10mas10m0s).TestFluxKinds_DurationsMatchMarkersholds each kind’s list of durations to its type and to the pattern markers of the linked source: theintervalof a Receiver, of an ImagePolicy and of an ImageUpdateAutomation, and theintervalandtimeoutof a Provider and of an ImageRepository, and thewindowof a ResourceSetInputProvider’s schedule; an Alert and an ArtifactGenerator have none. A schedule’swindowis held by its authored text: it sits under a list, and a window of500usin any schedule is refused (resourcesetinputprovider: schedule[].window "500us" is invalid: must be a Flux duration (…)); its pattern takesh, so one is written as Go formats it (90mas1h30m0s). Thetimeoutof a Provider and of an ImageRepository takes noh: its pattern holds the unitsms,sandm, so1his refused (imagerepository: timeout "1h" is invalid: must be a Flux duration (…)) and60mis not. Go formats an hour or more with anh, which the pattern would refuse at apply, so such a timeout is written in minutes (90mas90m0s), as the timeout of a Flux source is (go-kure/launcher#619).
Required is a field the API requires that the Go type writes whether or not it was authored, the rule every kind follows (see the Prometheus operator’s kinds below). Each must be authored (
providerRef: required (…),eventSources[1].name: required (…)); an authored empty value is a value, and the API server’s to refuse.- A
fluxcd-alert:providerRefwith itsname, andeventSources; of each source itskindandname. - A
fluxcd-provider:type; of an authoredsecretRef,proxySecretReforcertSecretRefitsname. - A
fluxcd-receiver:typeandresources; of each resource itskindandname; of an authoredsecretRefitsname; of each OIDC provider itsissuerURLandvalidations, of each validation itsexpressionandmessage, of each variable itsnameandexpression. - An
imagepolicy:imageRepositoryRefwith itsname, andpolicy; of asemverpolicy itsrange. - An
imagerepository:interval; of an authoredsecretRef,proxySecretReforcertSecretRefitsname; of an authoredaccessFromitsnamespaceSelectors.imageis required too, by another rule: the API requires it and the Go type leaves an empty one out, so the kind refuses a component without it, and one whoseimageis authored empty (image: required (…)). - An
imageupdateautomation:sourceRefwith itskindandname, andinterval. Of an authoredgit,commitwith itsauthorand the author’semail; of an authoredgit.checkout, itsref; of an authoredgit.commit.signingKey, itssecretRefwith itsname. Of a match expression ofpolicySelector, itskeyand itsoperator(policySelector.matchExpressions[0].operator: required (…)); itsvaluesand the operator’s value are the API server’s. - An
artifactgenerator:sourcesandartifacts; of each source itsalias,kindandname; of each artifact itsnameandcopy; of each copy itsfromandto. - A
resourcesetinputprovider:type; of an authoredsecretReforcertSecretRefitsname; of each schedule itscron. Of a match expression of a selector, itskeyand itsoperator(selectors[0].matchExpressions[0].operator: required (…)); itsvaluesand the operator’s value are the API server’s.
The lists are read from the markers of the Go source, not from a CRD. The API modules of the Flux controllers hold the Go types and ship no CRD; the Flux Operator’s module ships its CRDs, and its list is read the same way.
TestFluxKinds_RequiredMatchMarkersderives each list from the+requiredmarkers of the linked modules’ source, as the kinds of the Prometheus operator’s API are derived: every field so marked that the type writes unauthored is listed, and nothing else is. Thekeyandoperatorof a match expression belong to a Kubernetes type with no such marker, and are read from its source by the schema generators’ rule, as there: a field with no optional marker whose json tag keeps it when empty is required. A dependency bump that adds, drops or moves one fails there. Nothing here is held to the API server’s own validator, which answers from a CRD. Not refused:- a list the API wants an item of that is authored empty
(the
sources,artifactsandcopyof anartifactgenerator), and one longer than the API allows; - every value rule of the API but the pattern of a duration: enumerations
(
eventSeverity, a source’skind,digestReflectionPolicy, a policy’sorder), lengths (a source’snameandnamespace,summary), and that a source withmatchLabelsis named*, which the type documents and no marker states; - a
policythat names none ofsemver,alphabeticalandnumerical, or more than one: the type calls it a union, and no marker holds it to one; - of a
fluxcd-provider, the enumeration oftype, the lengths ofchannel,username,addressandproxy, and the pattern ofproxy(anhttporhttpsURL); anaddressthat is no URL with a host is written as authored unless it holds an@(Policy, below); - of a
fluxcd-receiver, the enumerations (type, a resource’skind), the lengths of a resource’snameandnamespace, the pattern of anissuerURL(anhttporhttpsURL; one that parses with no host is written as authored unless it holds an@, and one that does not parse is refused without naming the value), avalidationslist authored empty, two OIDC providers of oneissuerURL, which the type keys the list by, and that a resource withmatchLabelsis named*, which the type documents and no marker states; - of an
imagerepository, the enumeration ofprovider, the length ofserviceAccountName, anexclusionListof more than 25 entries, anaccessFromwhosenamespaceSelectorsis authored empty, and whetherimageis a repository name the controller accepts: one that carries a tag or a digest is written as authored; - an
imageupdateautomationwith nogit, which the type documents as “technically optional, but in practice mandatory” and no marker requires; the pattern ofgit.push.refspecand the enumerations (sourceRef.kind,update.strategy, a signing key’stype); - of an
artifactgenerator, the patterns (a source’salias,nameandnamespace,pathPattern, an artifact’srevisionandoriginRevision, a copy’sfromandto), the lengths and the enumerations (a source’skind, a copy’sstrategy); that an alias is unique and that a path, arevisionor anoriginRevisionnames an alias that is declared, which the type documents and no marker states; - of a
resourcesetinputprovider, the enumeration oftype, the pattern ofurl(anhttp,httpsorociURL) and the maximum offilter.limit(10000); theurlscheme each type takes is an expression rule (below).
The APIs’ expression rules are not checked. A kind checks an expression rule only where the check is held to the API server’s own validator, which answers from a CRD (go-kure/launcher#874), and the linked modules of the Flux controllers’ APIs ship none. The Flux Operator’s module does ship its CRDs, and the
resourcesetinputproviderkind deliberately checks none of their rules: it leaves every one to the API server, as the other Flux kinds do. A component that breaks one of the rules below builds, and the API server refuses the object at apply.TestFluxKinds_ExpressionRulesreads every such rule from the markers of the linked source and holds the list (fluxRulesLeft) to them, andTestFluxOperatorKinds_ExpressionRulesMatchCRDholds the ResourceSetInputProvider’s list to thex-kubernetes-validationsof the shipped CRD, in both directions, so a dependency bump that adds, drops or rewords one fails there.- The types of an Alert, of an ImageRepository and of an ImageUpdateAutomation declare none.
- A
fluxcd-provider:commitStatusExprset on atypeother thangithub,gitlab,gitea,bitbucketserver,bitbucketandazuredevops. It builds and is refused at apply. - A
fluxcd-receiver: ageneric-oidcreceiver without an OIDC provider or with asecretRef, and a receiver of another type withoidcProvidersor without asecretRef. Each builds and is refused at apply. - An
imagepolicy:intervalwithoutdigestReflectionPolicy: Always, anddigestReflectionPolicy: Alwayswithoutinterval. Each builds and is refused at apply. - An
artifactgenerator: where nopathPatternis set, every artifact’snamemust be a Kubernetes object name (lower case letters, digits,-and.). One that is not,App_Manifestsfor one, builds and is refused at apply. - A
resourcesetinputprovider: seventeen rules, sixteen of them on the spec and one on a selector.urlmust be left out underStaticandExternalArtifactand written under every other type, with the scheme the type takes (httporhttpsfor a Git, AzureDevOps andExternalServiceprovider,httpsfor AWSCodeCommit,ociwith a repository after the registry for an OCI provider);httpsfor anExternalServiceprovider unlessinsecureis true, andinsecureonly underExternalServiceandOCIArtifactTag;serviceAccountNameonly under the AzureDevOps, AWSCodeCommit, artifact-tag andExternalArtifacttypes;secretRefandcertSecretRefnot under the types that take none;selectorswritten underExternalArtifactand only there; and a selector’snamenot besidematchLabelsormatchExpressions. Each builds and is refused at apply.
Policy. Two dimensions of the environment policy reach three of these objects: the allowed registries hold the
imageof animagerepositoryand the host of aresourcesetinputprovider’surl, and a policy that forbids explicit secrets refuses afluxcd-provideraddressthat is the credential (below). No other dimension reaches any of them: they run no pod, request no storage and have no replica count.- No field of an Alert holds a secret or a host. The address and the
credentials are the Provider’s.
eventMetadatais a free map, written to the object as authored under a policy that forbids explicit secrets too. - A user or a password in a Provider’s
addressorproxyis refused, under every policy and under none, by the rule that refuses one in an inline chart source’s URL (fluxcd-provider: address must not carry a user or password, which would be written in plain text into the Provider; …): it would sit in the object in plain text. Anaddressor aproxythat is no URL with a host is refused when it holds an@(anaddresswith no@passes, since the API allows a project ID or a namespace there);bot:pw@hostandbot@hostparse as URLs with no user, so parsing alone does not clear them. - An
addressthat is the credential is refused under a policy that forbids explicit secrets (RefusalExplicitSecret;fluxcd-provider: address: the address of a msteams Provider is the credential it posts with, …), since the object is in the build’s output; the message names the type and quotes nothing of the value. Read in the notification controller’s source at v1.9.4 (internal/notifier):discord,rocket,msteams,googlechatandlarkpost to the address with no token of their own, so it is a webhook URL whose path or query holds the token;slackdoes the same where its Secret holds no token, and the kind does not read the Secret, so aslackaddress is refused even at the public API endpoint a token-holding Provider names;genericandgeneric-hmacpost to the address as written, and nothing tells an endpoint from one whose path or query is the credential. Anazureeventhubaddress that holdsSharedAccessKeyis a connection string with its key, which the notifier connects with as written; any other is an endpoint and passes. The remedy the message names: omitaddress, setsecretRef, and put the URL under theaddresskey of that Secret, which the controller reads in place ofaddressfor every type (internal/server/event_handlers.go). An emptyaddresspasses, and so does everyaddressunder a policy that allows explicit secrets, one that does not answer the question, and none.TestProviderAddressCredentialTypes_InUpstreamEnumholds the list to the API’s values oftype. A token in the path or the query of an address of another type is not something the kind can tell, and is written as authored. Not held: the hosts ofaddressandproxy: the Provider sends events there and fetches no artifact, so no dimension of the policy speaks of them;serviceAccountName. The credentials, the proxy configuration and the certificates ofsecretRef,proxySecretRefandcertSecretRefare Secrets the object names, not values. - No field of a Receiver holds an image or a secret. The token is in
the Secret
secretRefnames. A user or a password in an OIDC provider’sissuerURLis refused, under every policy and under none, by the same rule as a Provider’sproxy(fluxcd-receiver: oidcProviders[1].issuerURL must not carry a user or password, which would be written in plain text into the Receiver; …), and so is one that does not parse, or is no URL with a host and holds an@; the refusal names the provider’s index, not the value. The host of an OIDC provider’sissuerURLis not held: the controller discovers an issuer there and fetches no artifact, so no dimension of the policy speaks of it. That a Receiver opens an inbound path is not a dimension of the policy either. - No field of an ImagePolicy holds an image, a secret or a host. The image and the credentials of its registry are the ImageRepository’s; the policy holds the rule by which one of that image’s tags is selected. The allowed registries and the tag rule of the environment policy are not applied to it.
- The registry of an ImageRepository’s
imageis held to the allowed registries, by the rule that holds the image of a pod: under a policy that allowsregistry.example,image: ghcr.io/shop/webis refused (image "ghcr.io/shop/web" is not from an allowed registry [registry.example], a refusal of the classregistry). A name with no registry is one ofdocker.io, and a registry with a port is another registry than the one without. A policy that lists no registry refuses none, and neither does a build without a policy. The tag rule is not applied: the field names a repository, and the tags are what the scan finds. Not held: the host of the proxy, which is in the SecretproxySecretRefnames;insecure, which no dimension of the policy speaks of;providerandserviceAccountName. The credentials, the proxy configuration and the certificates are Secrets the object names, not values, so a policy that forbids explicit secrets has nothing of it to refuse. - No field of an ImageUpdateAutomation holds a secret or a host. The
address of the repository and its credentials are the GitRepository’s,
and the signing key is a Secret named by
git.commit.signingKey, not a value.git.commit.messageTemplateValuesandgit.push.optionsare free maps, written to the object as authored under a policy that forbids explicit secrets too. The images the automation writes into the repository are not held to the allowed registries or the tag rule: they are what the ImagePolicies select at run time, and no build sees them. - No field of an ArtifactGenerator holds a secret or a host. The
addresses and the credentials are those of the sources it names.
commonMetadataholds two free maps, written to the object as authored under a policy that forbids explicit secrets too, and held to the reserved metadata keys alone at generation. What an artifact carries is not checked: the copy is the controller’s to perform, and no build sees the files, so the rules a policy holds a workload or a Secret to do not reach manifests that travel inside an artifact. - A user or a password in a ResourceSetInputProvider’s
urlis refused, under every policy and under none, by the same rule (resourcesetinputprovider: url must not carry a user or password, which would be written in plain text into the ResourceSetInputProvider; …): the credentials belong in the SecretsecretRefnames. Aurlthat is no URL with a host is refused too when it holds an@, as a Provider’sproxyis. The host ofurlis held to the allowed registries, whatever the type: the answer of that host decides what a ResourceSet deploys, theExternalServicecall sends the referenced credential to it, and one rule holds a type a later operator version adds. Anoci://url is held by the rule that holds anocirepository’s: under a policy that lists registries it must name its registry explicitly and a repository after it. Every other url is held by the host rule of the Flux sources: under a policy that allowsregistry.example,url: https://git.other.example/shop/fleetis refused, a refusal of the classregistry. AStaticorExternalArtifactprovider has nourland nothing is held. A policy that lists no registry refuses none, and neither does a build without a policy. Not held:serviceAccountName,insecureand the selectors of anExternalArtifactprovider, which name no host; the inputs the provider returns, which the operator reads at run time and no build sees.defaultValuesis a free map, written to the object as authored under a policy that forbids explicit secrets too.
Namespace. The object lands in the Flux namespace when one is configured, else in the build namespace (
SetFluxNamespace), as the Flux kinds below do, and its name is claimed there. A reference without a namespace of its own is written without one, under a Flux namespace too: launcher fills none in. The Flux objects of the same document (helmrelease,fluxcd-kustomization, the sources) land in the Flux namespace with it.FluxNamespaceReadsreports the ConfigMaps and Secrets a kind reads by name from the namespace it lands in, so that a trait’s object one of them names moves with it; an Alert, an ImagePolicy and an ArtifactGenerator read none, a Provider and an ImageRepository read the Secrets ofsecretRef,proxySecretRefandcertSecretRef, a Receiver reads the Secret ofsecretRef, a ResourceSetInputProvider reads the Secrets ofsecretRefandcertSecretRef, and an ImageUpdateAutomation reads the Secret ofgit.commit.signingKey.secretRef. The ServiceAccount of a Provider’s, an ImageRepository’s or a ResourceSetInputProvider’sserviceAccountNameis not reported: the report holds ConfigMaps and Secrets only.Labels and annotations are the
labelsandannotationsproperties.Not covered. Whether what is referred to exists (the Provider, the objects of a source, the ImageRepository, the GitRepository, the signing key’s Secret, the sources of an ArtifactGenerator, the Secrets and the ServiceAccount of a Provider and of an ImageRepository, the resources and the Secret of a Receiver, the Secrets, the ServiceAccount and the ExternalArtifacts of a ResourceSetInputProvider), whether the registry of an ImageRepository answers and holds the repository, whether the address of a Provider, the issuer of a Receiver or the
urlof a ResourceSetInputProvider answers, whether acommitStatusExpror a Receiver’s CEL expression compiles, whether afilterTagspattern, asemverrange, a commit message template, apathPattern, a ResourceSetInputProvider’s filter expression or a schedule’scronparses, and whether the cluster serves the API: the component builds where the CRD is not installed, and the object is refused at apply. The object’s status is the controller’s and is not written.cnpg-imagecatalog, cnpg-clusterimagecatalog, cnpg-backup, cnpg-scheduledbackup, cnpg-databaserole, cnpg-publication, cnpg-subscription (go-kure/launcher#790) are the kind-named projections of seven objects of the CloudNativePG API,
postgresql.cnpg.io/v1, beyond those of cnpg-cluster and cnpg-pooler, below: an ImageCatalog, a ClusterImageCatalog, a Backup, a ScheduledBackup, a DatabaseRole, a Publication and a Subscription. Each emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default of its own. The ClusterImageCatalog is cluster-scoped: the object carries no namespace, whatever namespace the application is built for, and its name is claimed cluster-wide. The other six are namespaced and are written in the build namespace. No rule lowers onto these kinds:postgresqlemits none of them.No capability is required, and none gates these kinds: where the CloudNativePG CRDs are not installed the component builds. Whoever may author a component may author these, and with them the images the Clusters of a namespace or of the whole cluster may run, a role with any attribute PostgreSQL has (
superuser,bypassrls,replication), and what a database publishes or subscribes to. A policy can keep the kinds out of a build through the object kind policy (oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the top-level json fields of the spec type, decoded strictly at every depth: an unknown key is refused wherever it sits (an image, an extension, a plugin configuration, a target’s object).
cnpg-imagecatalogandcnpg-clusterimagecatalog(ImageCatalogSpec, which the two objects share):images, each animage, amajorand itsextensions(aname, animagevolume source with itsreferenceandpullPolicy, the four path lists andenv), andcomponentImages, each akeyand animage.cnpg-backup(BackupSpec):cluster,target,method,pluginConfiguration(name,parameters),onlineandonlineConfiguration.cnpg-scheduledbackup(ScheduledBackupSpec): the six of a Backup, andschedule,suspend,immediateandbackupOwnerReference.cnpg-databaserole(DatabaseRoleSpec):cluster,databaseRoleReclaimPolicy,clientCertificate, and the fields of the role configuration the type embeds:name,comment,ensure,passwordSecret,disablePassword,connectionLimit,validUntil,inRoles,inherit,superuser,createdb,createrole,login,replicationandbypassrls.cnpg-publication(PublicationSpec):cluster,name,dbname,parameters,target(allTables, orobjects, each atablesInSchemaor atablewith itsname,schema,onlyandcolumns) andpublicationReclaimPolicy.cnpg-subscription(SubscriptionSpec):cluster,name,dbname,parameters,publicationName,publicationDBName,externalClusterNameandsubscriptionReclaimPolicy.- No default is filled. An unauthored
method,ensure,inherit,backupOwnerReferenceor reclaim policy is left out and the API server fills its own. - A role’s
connectionLimit: 0is refused (connectionLimit: 0 cannot be carried by the CloudNativePG API types): the field is no pointer and is omitted when zero, and the CRD’s default,-1(no limit), would apply in its place, so a role authored to accept no connection would accept any number. - An empty string is refused on the seven string fields the CRDs
default (
method: "" cannot be carried by the CloudNativePG API types (the field is omitted when zero, so the operator would apply its default "barmanObjectStore")):methodon a Backup and a ScheduledBackup (barmanObjectStore), a ScheduledBackup’sbackupOwnerReference(none), a role’sensure(present) anddatabaseRoleReclaimPolicy(retain), andpublicationReclaimPolicyandsubscriptionReclaimPolicy(retain). The type omits an empty string, so the default would replace the authored value. WithconnectionLimitthese are every such field of the seven specs;TestCnpgKindsDefaultedZeroFields_MatchCRDderives each kind’s list from the linked CRDs and fails on another. - A role’s
validUntilis written in UTC. The API type holds an instant:2030-01-01T02:00:00+02:00is written2030-01-01T00:00:00Z. It writes the instant to the second and the zero time as null, which the API server drops, so avalidUntilwith a fraction of a second other than zero is refused (validUntil "2030-01-01T00:00:00.5Z": a fraction of a second cannot be carried by the CloudNativePG API types), after a period or a comma, read from the authored string, since the decode keeps only nine digits of a fraction; and so is0001-01-01T00:00:00Z(validUntil: the zero time cannot be carried by the CloudNativePG API types), which would leave a password that never expires. The time as the API type writes it is held to the CRD’sdate-timeformat and to the decoded instant, so an instant outside the four-digit years in UTC is refused whatever its authored offset:9999-12-31T23:00:00-02:00(validUntil: the CloudNativePG API types write it as 10000-01-01T01:00:00Z, which the DatabaseRole CRD's date-time format refuses).
Required follows the rule of the other kinds built on
policyFreeKind: a field the API requires that the Go type writes whether or not it was authored. Each must be authored (images[1].major: required (…)):- a catalog’s
images; an image’simageandmajor; an extension’sname, and thenameand thevalueof each of itsenventries; a component image’skeyandimage; cluster, on the five kinds that refer to a Cluster, and itsnameon acnpg-backupand acnpg-scheduledbackup. The DatabaseRole, Publication and Subscription CRDs default thenameto an empty string, which no Cluster has, so the other three refuse a reference with no name on launcher’s own account, with the samecluster.name: required;- a plugin configuration’s
name, on acnpg-backupand acnpg-scheduledbackupthat author one; - a
cnpg-scheduledbackup’sschedule; - a
cnpg-databaserole’sname, and thenameof an authoredpasswordSecret; - a
cnpg-publication’sname,dbnameandtarget, and thenameof a target’stable; - a
cnpg-subscription’sname,dbname,publicationNameandexternalClusterName.
TestCnpgFurtherKinds_RequiredMatchCRDholds these lists to the fields the linked module’s CRD requires and the type writes unauthored, so a dependency bump that adds, drops or moves one fails there. An authored empty value satisfies the rule and is the API server’s to refuse (schedule: ""), with three exceptions the build refuses itself: acluster.namemust be a name CloudNativePG admits for a Cluster (a DNS-1035 label of at most 50 characters), as oncnpg-poolerandcnpg-database; a role’sname: ""breaks a rule of its CRD (below); and a catalog’simage: "", on an image or a component image, is no image reference and is refused by the tag rule (below).The CRDs’ expression rules. The seven CRDs declare 26. The build checks the 15 that read one document, each a comparison of authored fields with each other or with a string the CRD itself states, so a document that breaks one could never be admitted:
- a catalog (two rules, on each of the two CRDs): no two images of one
major(images[1].major: 17 is also the major version of images[0]; the API takes each major version once), and no two component images of onekey. The component images are a list keyed onkey, so the API server refuses a key held twice by its schema as well as by the rule; - a role (eight): a
namethat is empty,postgresorstreaming_replica, or that starts withpg_orcnpg_(name "postgres": reserved, the DatabaseRole CRD refuses it);ensure: absent, which the API does not take on a DatabaseRole (a role is removed by deleting the object withdatabaseRoleReclaimPolicy: delete);passwordSecretbesidedisablePassword: true; and a client certificate that is enabled withoutlogin: true. A certificate is enabled where its block is authored andenabledis notfalse: the API fillstrueinto an omitted one before it evaluates the rule. It fills nothing intologin, which the rule reads without asking whether it is there: an enabled certificate with nologinauthored is refused by the API server as a rule it cannot evaluate (no such key: login), and one besidelogin: falseby the rule’s own message. The build gives both the one refusal; - a publication (three): a
targetthat publishes neither or both ofallTables: trueandobjects(target: one of allTables: true and objects is required); an object that is neither or both oftablesInSchemaandtable; and a table that lists itscolumnswhere another object publishestablesInSchema. The Go type leaves out a falseallTablesand an emptyobjects,tablesInSchemaorcolumns, so each is read as the object will hold it:allTables: falsebeside a list of objects builds, and is a list of objects. The API server reads the document it is sent, so the two differ on a document that authors one of the four, and agree on the object: sent as authored,allTables: falsebeside a list of objects is refused, and the object the build writes for it is accepted;allTables: falsealone is accepted as authored and refused by the build, since the object written holds neither.TestCnpgPublication_TargetAsWrittenshows the first for each of the four, and the second for the three that can stand alone (an emptycolumnsalone is a table that lists none).
The other eleven refuse a change of the stored object, which a build does not have, and stay the API server’s: a Backup’s whole
spec; theclusterof a ScheduledBackup, a DatabaseRole, a Publication and a Subscription; thenameof a role; thenameand thedbnameof a publication and of a subscription; and a publication’starget.allTables, from one authored value to another. Such a change builds here and is refused at apply, until the object is deleted and created again. The rule onallTablessits on that optional field, and the API server does not evaluate it where the field is absent from the stored object or from the new one: a target moved fromallTablestoobjects, or back, is not refused by it.TestCnpgFurtherKinds_ExpressionRuleslists all 26 with what is done with each, and fails on a rule that is added, dropped or reworded. It holds each of the 15 to the API server’s own expression validator, run over the linked CRD after the defaults the API server fills: with every other rule set aside, the validator refuses the properties that break the rule, and with every rule it accepts the ones next to them and the object the build writes for them; the build answers the same on both.A Backup is a one-shot request. The operator takes the backup once, when the object is created, and the API refuses every later change of its
spec. Acnpg-backupkept in a repository and applied again is the same object: nothing runs, and a change to its properties is refused at apply. Where the repository is applied continuously, the API refuses the changed document at every apply, so the delivery fails on it each time until the change is taken back, or the request is authored as a new object (anotherobjectName), which is a new Backup and a new backup taken. What a delivery tool set to replace an object it cannot update does with that refusal is the tool’s, and is not read here. It suits a backup that is asked for once, under a name of its own (before an upgrade, say), and is then left as the record of that backup. A backup that recurs is acnpg-scheduledbackup, whose Backups the operator creates and names.Not checked, and the API server’s or the operator’s to refuse:
- the number of a catalog’s images (one to eight) and of its component
images (at most 32), a
majorbelow 10, and the forms of akey, of an extension’snameand of anenvname; - a
schedule: launcher does not parse it. The operator reads a cron expression of six fields, the first of which is the seconds; its webhook refuses a schedule that does not parse and only warns of one that parses with another number of fields; - what the operator’s webhook holds a Backup and a ScheduledBackup to: that
onlineandonlineConfigurationdo not come with thebarmanObjectStoremethod, which is also the method of a document that authors none, and, on a Backup only, that thepluginmethod comes with apluginConfiguration; parameterson a publication, a subscription and a plugin configuration: a free map of strings, written as authored;- every other value rule of the API: the enumerations (
method,target,backupOwnerReference,ensure, the reclaim policies, an image volume’spullPolicy). AvalidUntilis the exception: the strict decode reads an RFC 3339 time and refuses anything else.
The tag rule on a catalog’s images. The three fields of a catalog that name an image are held to
ValidateImageRef, as a container’s image is, when the component is read, with or without a policy: an image’simage, a component image’simageand thereferenceof an extension’s image volume. One without a tag or digest, or tagged:latest(with a digest too), or one that is no image reference, is refused by the field’s path (images[0].image: image "registry.example/team/postgresql" rejected: no tag or digest specified; use an explicit version tag or digest), and the refusal carries no policy class: a Cluster that takes its image from the catalog runs what the entry names. An extension that names no reference names no image and is not checked. A reference by digest alone passes; what CloudNativePG itself requires of a catalog’s image is the operator’s rule and is left to it. The rule runs at that read and nowhere else: generation builds the object from what the read decoded and does not repeat it.cnpg-clusterandcnpg-poolerrepeat theirs at generation because their config is an exported value a caller can change after the read; nothing but the read can make a catalog’s config.TestImageTagRule_CnpgImageCatalogsholds the three, on both kinds.Policy. The two catalogs are held to the environment policy; the other five have no field an
oam.Policymethod speaks to, and build the same under every policy and under none.- Every image a catalog names is held to the allowed registries. A
catalog runs no pod, but an image a Cluster takes from it is one the
operator runs.
ApplyPolicychecks the three fields that name one, and refuses by the field’s path with the classoam.RefusalRegistry: an image’simage(images[0].image), a component image’simage(componentImages[0].image) and thereferenceof an extension’s image volume (images[0].extensions[0].image.reference). An extension that names no reference names no image, and nothing is checked for it.TestCnpgImageCatalogs_ImagesAreHeldToTheAllowedRegistriesholds the three, on both kinds. - A Cluster’s reference to a catalog is a name. A
cnpg-cluster’simageCatalogRefnames a catalog and a major version, and holds no image: the host is read where the image is authored, on the catalog. A catalog that is not built with launcher is not seen. - No field holds a literal secret. A role’s
passwordSecretis the name of a Secret in the role’s namespace, and launcher emits no Secret for it; a subscription’s credentials are those of the external cluster the subscriber Cluster defines. A plugin’sparametersand an extension’senvare free text: a value written there is written into the object as authored and is not checked.
Labels and annotations are the
labelsandannotationsproperties, and the object also carries the component label (see “Component label and ownership” in the OAM model).Not covered. Whether what is referred to exists: the Cluster, the Secret, the database, the publication, the external cluster of a subscription, the plugin, the schema and the tables a publication names. A status of any of the seven is the operator’s and is not written. The Backups a ScheduledBackup creates, and the Secret a role’s client certificate is issued into (
<object name>-client-cert), are the operator’s too.metallb-bfdprofile (go-kure/launcher#790) is the kind-named projection of a fourth object of MetalLB’s
metallb.io/v1beta1API: a BFDProfile. It is built onpolicyFreeKindas the kinds above are and emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default of its own.What it changes for others. A profile is the settings of a BFD session: the intervals at which control packets are sent and expected (
receiveInterval,transmitInterval,echoInterval, in milliseconds), the multiplier that turns an interval into the time after which the connection to a peer is taken for lost (detectMultiplier), the echo and passive modes, and the minimum time-to-live of a multi-hop session’s packets (minimumTtl). On its own it changes nothing: a MetalLB BGPPeer names the profile its session uses (bfdProfile), and a peer that names none has no BFD session. The profile then decides how fast the loss of that peer is noticed.The object is namespaced, and MetalLB reads it in one namespace only, as it reads a pool: it is written in the build namespace, and a profile of an application built for another namespace than the one MetalLB watches is an object MetalLB does not read. Launcher does not know that namespace and checks nothing of it.
No capability is required, and none gates the kind: where MetalLB’s CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component of an application built for MetalLB’s namespace may author a profile, and may so change the timers of the peers that already name one of that name. A policy can keep the kind out of a build through the object kind policy (
oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the seven top-level json fields of
BFDProfileSpec, decoded strictly: an unknown key is refused. None is required, and a component that authors none builds, asspec: {}.- No default is filled, and every authored value is written. Each
field is a pointer: an authored
echoMode: falseorpassiveMode: falseis written, and so is an authored 0. The CRD declares no default. The values MetalLB uses for an unset field are MetalLB’s: the comments of its type name 300 ms for the receive and the transmit interval and 50 ms for the echo interval, which launcher neither fills nor checks.TestMetalLBKinds_NoDefaultIsLostfails on a default a dependency bump adds to a field that could not carry an authored empty value.
Required: nothing.
TestMetalLBKinds_RequiredMatchCRDholds that to the linked module’sv1beta1CRD.The CRD’s expression rules. The CRD declares none;
TestMetalLBKinds_ExpressionRulesfails on one that is added.Not checked, and MetalLB’s or the API server’s to refuse:
- MetalLB’s validating webhook was not read, and nothing it refuses is repeated here. An object the CRD’s schema takes may still be refused at apply by the webhook MetalLB installs;
- the bounds the CRD sets on the numbers (10 to 60000 for an interval, 2 to 255 for the multiplier, 1 to 254 for the time-to-live): a value outside them is written as authored;
- whether the modes fit the session: the type’s comment says the echo mode
is not supported on multi-hop setups, and
minimumTtlis for multi-hop sessions only.
Labels and annotations are the
labelsandannotationsproperties. A BGPPeer refers to the profile by its name: name the component so, or setobjectName.Policy. No field of the spec is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing, and the kind builds the same under every policy and under none. No field names a host or an image, and none holds a literal secret or refers to a Secret.Not covered. Whether a peer names the profile. The object’s status is MetalLB’s and is not written: the type has no field in it, and the YAML the library writes leaves an empty status out.
- No default is filled, and every authored value is written. Each
field is a pointer: an authored
metallb-bgpadvertisement (go-kure/launcher#790) is the kind-named projection of a third object of MetalLB’s
metallb.io/v1beta1API: a BGPAdvertisement. It is built onpolicyFreeKindas the two kinds above are and emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default of its own.What it changes for others. A BGPAdvertisement makes MetalLB announce to the cluster’s BGP peers the addresses it gave to Services from address pools, and sets what the routes carry. No field is required:
ipAddressPoolsnames the pools andipAddressPoolSelectorsselects them by their labels. With no pool named or selected the advertisement applies to every pool MetalLB reads;peerslimits the BGP peers the addresses are announced to. With none, they are announced to every BGP peer MetalLB is configured with;nodeSelectorslimits the nodes that are announced as next hops for an address. With none, the advertisement excludes no node;serviceSelectorslimits the Services. With none, every Service that has an address of the selected pools is announced;aggregationLengthandaggregationLengthV6roll the addresses up into a larger prefix, of that length, which is then what is announced in place of one route per address;localPrefandcommunitiesare attributes of the announcement: BGP’s best path selection prefers a path with a higher LOCAL_PREF, and the communities are attached to what the peers receive.
A component that authors no property is the widest advertisement there is: every pool, to every peer, for every Service, and no node excluded. It builds, as
spec: {}.The object is namespaced, and MetalLB reads it in one namespace only, as it reads a pool: it is written in the build namespace, and an advertisement of an application built for another namespace than the one MetalLB watches is an object MetalLB does not read. Launcher does not know that namespace and checks nothing of it.
No capability is required, and none gates the kind: where MetalLB’s CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component of an application built for MetalLB’s namespace may author an advertisement, and with it which addresses the cluster announces to its BGP peers and with which attributes. A policy can keep the kind out of a build through the object kind policy (
oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the top-level json fields of
BGPAdvertisementSpec, decoded strictly at every depth: an unknown key is refused wherever it sits (the spec, a selector, a match expression).- No default is filled, and an authored value is written where the Go
type can hold it. The two aggregation lengths are pointers: an authored
one is written, also where it is the value the API would fill, and an
unauthored one is left out, which the API fills with 32 and 128, one
route per address.
TestMetalLBKinds_NoDefaultIsLostholds the defaults of the linked CRD to that. localPrefis a number the type omits at 0, with no default in the CRD: an authoredlocalPref: 0is left out, and is the same advertisement to MetalLB as one that does not author the field, since its type holds one value for both. That is not an advertisement without LOCAL_PREF: what MetalLB sends is its BGP backend’s choice (at MetalLB v0.16.1 the native backend sends LOCAL_PREF 0 on an iBGP session; the FRR backends set no local preference). An authored empty list is left out too, as the type omits it: it is the same advertisement as one that does not author the field.
Required: the
keyand theoperatorof a match expression, in every selector ofipAddressPoolSelectors,nodeSelectorsandserviceSelectors. The API requires nothing else of the spec.TestMetalLBKinds_RequiredMatchCRDholds the list to the linked module’sv1beta1CRD.The CRD’s expression rule is checked. The CRD declares one, on the spec: a service selector is taken only with one route per address. A component that authors a non-empty
serviceSelectorsbeside anaggregationLengthother than 32 or anaggregationLengthV6other than 128 is refused (serviceSelectors: not allowed with aggregationLength 24: …). The API server evaluates the rule after it has filled its defaults, which are those two values, so an unauthored length keeps the rule, as one authored at that value does, and so does an empty list of selectors.TestMetalLBKinds_ExpressionRulesholds the kind’s answer to the API server’s for the linked CRD, on properties that break the rule and on properties that keep it, and fails on a rule that is added or reworded.Not checked, and MetalLB’s or the API server’s to refuse:
- MetalLB’s validating webhook was not read, and nothing it refuses is repeated here. An object the CRD’s schema takes may still be refused at apply by the webhook MetalLB installs;
- the CRD’s other value rules: an
aggregationLengthbelow 1 is written as authored; - a community: no entry of
communitiesis read, neither its form nor whether a Community object defines the alias it names; - a pool’s name and a peer’s name: one that names no object of the application builds;
- an authored empty value in a required field (a
key: ""). It is a value.
Labels and annotations are the
labelsandannotationsproperties. The poolsipAddressPoolsnames are the object names ofmetallb-ipaddresspoolcomponents (the component’s name, or itsobjectName), and the labelsipAddressPoolSelectorsqueries are theirlabels. The other two selectors query objects launcher does not write the labels of here (nodes, Services) and are the author’s.Policy. No field of the spec is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing, and the kind builds the same under every policy and under none. No field names a host or an image, and none holds a literal secret or refers to a Secret.Not covered. Whether what is named or selected exists (a pool, a peer, a community alias, a node, a Service). The object’s status is MetalLB’s and is not written: the type has no field in it, and the YAML the library writes leaves an empty status out.
metallb-bgppeer (go-kure/launcher#790) is the kind-named projection of a sixth object of MetalLB’s API: a BGPPeer. It is built at
metallb.io/v1beta2, not at thev1beta1of the five kinds above: that is the version the linked module’s CRD stores,v1beta1is served and marked deprecated there, and the base library’s constructor builds the peer atv1beta2.TestMetalLBKinds_EmitTheServedVersionholds each kind to the version its CRD serves and stores. The kind is built onpolicyHeldKind, the helper of the kinds above with one check under the environment policy, and emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default of its own.What it changes for others. A BGPPeer is a router MetalLB holds a BGP session with, from the nodes that match one of the
nodeSelectors, and from every node where the list is unset or empty; selectors that match no node leave the peer with no session. Over that session the cluster announces the addresses its BGP advertisements cover: an advertisement that names no peer announces to every peer, so a new peer receives those announcements without any advertisement being changed. The fields say who the peer is and how the session runs:myASNis the AS number of the cluster’s end,peerASNthe one expected of the router, ordynamicASN(internalorexternal) where it is detected;localASNannounces another AS number to this peer;peerAddressis the address dialled, orinterfacethe node’s interface of an unnumbered session;peerPort,sourceAddress,routerIDandvrfare the rest of the connection;holdTime,keepaliveTimeandconnectTimeare the session’s timers;passwordorpasswordSecretauthenticates the session (TCP MD5);bfdProfilenames the BFDProfile of the session’s BFD session, and a peer that names none has no BFD session;enableGracefulRestart,ebgpMultiHop,disableMP(deprecated upstream in favour ofdualStackAddressFamily) anddualStackAddressFamilyare switches of the session.
The object is namespaced, and MetalLB reads it in one namespace only, as it reads a pool: it is written in the build namespace, and a peer of an application built for another namespace than the one MetalLB watches is an object MetalLB does not read. Launcher does not know that namespace and checks nothing of it.
No capability is required, and none gates the kind: where MetalLB’s CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component of an application built for MetalLB’s namespace may author a peer, and with it a router that the cluster’s nodes connect to and that is told the addresses of the cluster’s Services. A policy can keep the kind out of a build through the object kind policy (
oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the twenty-one top-level json fields of
BGPPeerSpec, decoded strictly at every depth: an unknown key is refused wherever it sits (the spec, the Secret reference, a selector, a match expression).- No default is filled. The CRD declares three.
disableMPanddualStackAddressFamilydefault tofalse, which is the value the type leaves out: an authoredfalseis left out, and the API fills the samefalseback.peerPortdefaults to 179 and the type leaves a 0 out, so an authoredpeerPort: 0would become 179: it is refused (peerPort: 0 cannot be carried by the MetalLB API types (…)). An unauthored port is left out, and the API fills 179.TestMetalLBKinds_NoDefaultIsLostholds the kind’s list of such fields to the linked CRD. - An authored 0, false or empty string on any other field the type omits
when empty is left out, as the type holds one value for it and for a
peer that does not author the field:
peerASN,localASN, the two other switches, the addresses and names. ForlocalASNthat is a value under the CRD’s minimum of 1, which the API server therefore does not see. - The three timers are pointers: an authored
holdTime: 0sis written. A duration is written in the form the type gives it (90sas1m30s). myASNis always encoded: an authored 0 is written.passwordSecret: {}is written where no reference is authored. The type holds the reference by value and always encodes it. The CRD takes it, and MetalLB v0.16.1 reads a reference with no name as none.
Required:
myASN, and thekeyand theoperatorof a match expression in every selector ofnodeSelectors. The API requires nothing else of the spec.TestMetalLBKinds_RequiredMatchCRDholds the list to the linked module’sv1beta2CRD.Two of the CRD’s three expression rules are checked. Both are on
connectTime: it is from 1 to 65535 seconds, and it is a whole number of seconds. AconnectTimethat breaks one is refused (connectTime: 1.5s is not a whole number of seconds, which the API requires). The kind reads a duration as the API server’s rules do, in whole seconds and whole milliseconds with the rest dropped:65535.5spasses the first rule and breaks the second, and a part smaller than a millisecond is seen by neither, here or there.TestMetalLBKinds_ExpressionRulesholds the kind’s answer to the API server’s for the linked CRD, on properties that break each rule and on properties that keep it, and fails on a rule that is added or reworded. The third rule is left:enableGracefulRestartmay not change after creation. It compares the field with the stored object’s, which a build does not have, and the API server does not evaluate it on a create.Not checked, and MetalLB’s or the API server’s to refuse:
- MetalLB’s validating webhook was not read, and nothing it refuses is repeated here. An object the CRD’s schema takes may still be refused at apply by the webhook MetalLB installs;
- what MetalLB itself requires of a peer when it reads its configuration.
At v0.16.1 that is, among others: a
myASNother than 0; exactly one ofpeerASNanddynamicASN; exactly one ofpeerAddressandinterface; not bothpasswordand a namedpasswordSecret. A component that breaks one of these builds; - the CRD’s other value rules: a
peerPortabove 16384 and adynamicASNthat is neitherinternalnorexternalare written as authored; - an address, a router ID and a VRF’s or an interface’s name: none is read;
- the profile
bfdProfilenames and the SecretpasswordSecretnames: one that names no object of the application builds; - an authored empty value in a required field (a
key: ""). It is a value.
Labels and annotations are the
labelsandannotationsproperties. A BGPAdvertisement names the peers it announces to (peers): name the component so, or setobjectName. The profilebfdProfilenames is the object name of ametallb-bfdprofilecomponent (the component’s name, or itsobjectName).nodeSelectorsqueries nodes, whose labels launcher does not write, and is the author’s.Policy. The environment policy reaches one field.
passwordis the session’s password in the clear, and is refused under a policy that forbids explicit secrets (password: holds the BGP session password in the object, and the environment policy forbids explicit secrets; name a Secret created out of band in passwordSecret instead), as the keystore password of acertificateis. The refusal does not carry the value. Under a policy that allows explicit secrets, under one that does not answer the question and under none the peer builds, with the password in the object as authored. An emptypasswordis none.TestMetalLBBGPPeerKind_SessionPasswordholds this.passwordSecretnames a Secret and holds no secret, and builds under every policy. Launcher does not create that Secret and does not check that it exists. The upstream type’s comment asks for one of typekubernetes.io/basic-auth, in the namespace of the MetalLB deployment, with the password under the keypassword; none of that is checked, and neither is the reference’snamespace.- Hosts are not checked. The addresses a peer holds are a network’s, not an artifact source, and none is held to the policy’s allowed registries. No field names an image.
- No other
oam.Policymethod speaks to a field of the spec, andApplyPolicyfills nothing.
Not covered. Whether what is named or selected exists (a profile, a Secret, a node), and whether the router is reachable. The object’s status is MetalLB’s and is not written: the type has no field in it, and the YAML the library writes leaves an empty status out.
metallb-community (go-kure/launcher#790) is the kind-named projection of a fifth object of MetalLB’s
metallb.io/v1beta1API: a Community. It is built onpolicyFreeKindas the kinds above are and emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default of its own.What it changes for others. A Community defines aliases: each gives a
nameto one BGP communityvalue, a standard one of the form1234:1234or a large one of the formlarge:1234:1234:1234. On its own it announces nothing. An item of a MetalLB BGPAdvertisement’scommunitiesmay be the name of such an alias, and the advertisement then attaches the alias’s value to what the cluster announces to its BGP peers. Routers that receive a route may act on its communities, so the value behind a name bears on how the networks beyond the cluster treat the addresses announced under it.The object is namespaced, and MetalLB reads it in one namespace only, as it reads a pool: it is written in the build namespace, and a Community of an application built for another namespace than the one MetalLB watches is an object MetalLB does not read. Launcher does not know that namespace and checks nothing of it.
No capability is required, and none gates the kind: where MetalLB’s CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component of an application built for MetalLB’s namespace may author a Community, and may so define a name an advertisement there already uses. What MetalLB does with a name that two aliases define, in one object or in two, was not read. A policy can keep the kind out of a build through the object kind policy (
oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The one property is the top-level json field of
CommunitySpec,communities, decoded strictly: an unknown key is refused, in the spec and in an alias. It is not required, and a component that authors none builds, asspec: {}.- No default is filled. The CRD declares none. The
nameand thevalueof an alias are strings the upstream type leaves out when empty, and the CRD requires neither: an alias authored with an empty one is written without it, and an alias that authors nothing is written as an empty item.TestMetalLBKinds_NoDefaultIsLostfails on a default a dependency bump adds to a field that could not carry an authored empty value.
Required: nothing.
TestMetalLBKinds_RequiredMatchCRDholds that to the linked module’sv1beta1CRD.The CRD’s expression rules. The CRD declares none;
TestMetalLBKinds_ExpressionRulesfails on one that is added.Not checked, and MetalLB’s or the API server’s to refuse:
- MetalLB’s validating webhook was not read, and nothing it refuses is repeated here. An object the CRD’s schema takes may still be refused at apply by the webhook MetalLB installs;
- the form of a value: one that is neither a standard nor a large community is written as authored;
- an alias without a name or without a value, and a name defined twice.
Labels and annotations are the
labelsandannotationsproperties. An advertisement refers to an alias by the alias’sname, not by the name of the object that holds it.Policy. No field of the spec is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing, and the kind builds the same under every policy and under none. No field names a host or an image, and none holds a literal secret or refers to a Secret.Not covered. Whether an advertisement names an alias, and whether a name an advertisement uses is defined. The object’s status is MetalLB’s and is not written: the type has no field in it, and the YAML the library writes leaves an empty status out.
- No default is filled. The CRD declares none. The
metallb-ipaddresspool (go-kure/launcher#790) is the kind-named projection of an object of MetalLB’s
metallb.io/v1beta1API: an IPAddressPool. It is built onpolicyFreeKindand emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default of its own. The type name carries themetallb-prefix as the kinds of Cilium’s API carry theirs: both APIs have a pool of addresses and an advertisement for BGP.What it changes for others. A pool is the address ranges MetalLB has authority over and gives to Services of type LoadBalancer. With no
serviceAllocationthe pool is limited to no namespace and no Service: a Service of any namespace of the cluster may be given one of its addresses.serviceAllocationlimits it to the namespaces it lists or selects and to the Services it selects.autoAssign: falsekeeps MetalLB from allocating from the pool on its own.The object is namespaced, and MetalLB reads it in one namespace only. It is written in the build namespace. MetalLB reads the objects of its API in one namespace and in no other: the one it is configured to watch, by its
--namespaceflag or theMETALLB_NAMESPACEvariable, and by default the one it runs in. A pool of an application built for another namespace is an object MetalLB does not read. Launcher does not know that namespace and checks nothing of it.No capability is required, and none gates the kind: where MetalLB’s CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component of an application built for MetalLB’s namespace may author a pool, and with it which Services of the cluster get an address of which range. A policy can keep the kind out of a build through the object kind policy (
oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the top-level json fields of
IPAddressPoolSpec, decoded strictly at every depth: an unknown key is refused wherever it sits (the spec, the allocation, a selector).addresses, each a CIDR prefix or a first and a last address joined by a dash;autoAssign;avoidBuggyIPs;serviceAllocation, with itspriority,namespaces,namespaceSelectorsandserviceSelectors.- No default is filled, and an authored value that is the API’s default is
not written where the Go type cannot hold it.
autoAssignis a pointer: an authoredfalseis written, and an unauthored one is left out, which the API fills withtrue.avoidBuggyIPsis no pointer and is omitted when false: an authoredfalseis left out of the object, and the API fills the samefalseback.TestMetalLBKinds_NoDefaultIsLostholds the defaults of the linked CRD to that: one that is not the field’s empty value, on a field that is no pointer, fails there. An allocation’spriorityis a number the type omits at 0, with no default in the CRD: an authoredpriority: 0is an allocation with no priority, as MetalLB’s own type reads it.
Required: a field the API requires that the Go type writes whether or not it was authored must be authored (
addresses: required (…)):addresses;- the
keyand theoperatorof a match expression, in every selector ofserviceAllocation.namespaceSelectorsandserviceAllocation.serviceSelectors.
An authored empty value satisfies the rule:
addresses: []is a pool with no address, which the CRD’s schema takes.TestMetalLBKinds_RequiredMatchCRDholds the list to the fields the linked module’sv1beta1CRD requires and the type writes unauthored, so a dependency bump that adds, drops or moves one fails there.The CRD’s expression rules. The CRD declares none;
TestMetalLBKinds_ExpressionRulesfails on one that is added.Not checked, and MetalLB’s or the API server’s to refuse:
- MetalLB’s validating webhook was not read, and nothing it refuses is repeated here. An object the CRD’s schema takes may still be refused at apply by the webhook MetalLB installs;
- an address: no entry of
addressesis read, and a string that is no range builds; - an authored empty value in a required field (a
key: ""). It is a value.
Labels and annotations are the
labelsandannotationsproperties. A pool is referred to by its name, or selected by its labels. A MetalLB advertisement names the pools it announces (ipAddressPools): name the component so, or setobjectName. ItsipAddressPoolSelectorsis a label query over pools: write the labels it names under thelabelsof themetallb-ipaddresspool. The object also carries the component label, whose value is the component’s (see “Component label and ownership” in the OAM model). The selectors ofserviceAllocationquery objects launcher does not write the labels of here (namespaces, Services) and are the author’s.Policy. No field of the spec is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing, and the kind builds the same under every policy and under none.- Hosts are not checked. No field names a host. An address or a range a pool holds is a network’s, not an artifact source, and none is held to the policy’s allowed registries.
- No field holds a literal secret or refers to a Secret, and none is checked.
Not covered. Whether what is selected exists (a namespace, a Service). The pool’s status is MetalLB’s and is not written: the Go type always encodes its four counters, at zero, and the YAML the library writes leaves a status that holds nothing else out.
metallb-l2advertisement (go-kure/launcher#790) is the kind-named projection of another object of MetalLB’s
metallb.io/v1beta1API: an L2Advertisement. It is built onpolicyFreeKindasmetallb-ipaddresspoolis and emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default of its own.What it changes for others. An L2Advertisement makes MetalLB announce on the local network (layer 2) the addresses it gave to Services from address pools. Every field narrows it, and none is required:
ipAddressPoolsnames the pools andipAddressPoolSelectorsselects them by their labels. With no pool named or selected the advertisement applies to every pool MetalLB reads;nodeSelectorslimits the nodes that are announced as next hops for an address. With none, the advertisement excludes no node;interfaceslists the interfaces of a node the address is announced from. With none, it is announced from all of them;serviceSelectorslimits the Services. With none, every Service that has an address of the selected pools is announced.
A component that authors no property is the widest advertisement there is: every pool, every interface, every Service, and no node excluded. It builds, as
spec: {}.The object is namespaced, and MetalLB reads it in one namespace only, as it reads a pool: it is written in the build namespace, and an advertisement of an application built for another namespace than the one MetalLB watches is an object MetalLB does not read. Launcher does not know that namespace and checks nothing of it.
No capability is required, and none gates the kind: where MetalLB’s CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component of an application built for MetalLB’s namespace may author an advertisement, and with it which addresses the cluster announces on its local network, from which nodes and interfaces. A policy can keep the kind out of a build through the object kind policy (
oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the top-level json fields of
L2AdvertisementSpec, decoded strictly at every depth: an unknown key is refused wherever it sits (the spec, a selector, a match expression). The CRD declares no default on any of them, and none is filled. An authored empty list is left out, as the type omits it: it is the same advertisement as one that does not author the field.Required: the
keyand theoperatorof a match expression, in every selector ofipAddressPoolSelectors,nodeSelectorsandserviceSelectors(nodeSelectors[1].matchExpressions[0].key: required (…)). The API requires nothing else of the spec.TestMetalLBKinds_RequiredMatchCRDholds the list to the linked module’sv1beta1CRD.The CRD’s expression rules. The CRD declares none;
TestMetalLBKinds_ExpressionRulesfails on one that is added.Not checked, and MetalLB’s or the API server’s to refuse:
- MetalLB’s validating webhook was not read, and nothing it refuses is repeated here. An object the CRD’s schema takes may still be refused at apply by the webhook MetalLB installs;
- a pool’s name: no entry of
ipAddressPoolsis read, and one that names no pool of the application builds; - an interface’s name;
- an authored empty value in a required field (a
key: ""). It is a value.
Labels and annotations are the
labelsandannotationsproperties. The poolsipAddressPoolsnames are the object names ofmetallb-ipaddresspoolcomponents (the component’s name, or itsobjectName), and the labelsipAddressPoolSelectorsqueries are theirlabels. The other two selectors query objects launcher does not write the labels of here (nodes, Services) and are the author’s.Policy. No field of the spec is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing, and the kind builds the same under every policy and under none. No field names a host or an image, and none holds a literal secret or refers to a Secret.Not covered. Whether what is named or selected exists (a pool, a node, an interface, a Service). The object’s status is MetalLB’s and is not written: the type has no field in it, and the YAML the library writes leaves an empty status out.
alertmanager (go-kure/launcher#790) is the kind-named projection of an Alertmanager of the Prometheus operator’s
monitoring.coreos.com/v1API. It is built onpolicyHeldKind(policyFreeKindwith a policy check, see storageclass, below) and emits that one object in the build namespace, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default, and declares the object as namespaced. Launcher emits no pod, no StatefulSet, no Service and no Secret for it: the operator builds a StatefulSet from the object and runs the pods. What the spec says of those pods is held to the environment policy as a workload kind’s own fields are.No capability is required, and none gates the kind. As for the four kinds under servicemonitor, below, launcher does not ask whether the cluster serves
monitoring.coreos.com/v1: where the operator’s CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component may author an Alertmanager, and so make the operator run pods in the namespace; the policy below is what holds them. A policy can keep the kind out of a build through the object kind policy (oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the top-level json fields of
AlertmanagerSpec, decoded strictly at every depth (an unknown key is refused wherever it sits: a container, a claim template, a web setting), less the three under “Not authorable”:- the image:
image,version,imagePullPolicy,imagePullSecrets; - the pods:
replicas,resources,storage,volumes,volumeMounts,containers,initContainers,securityContext,podMetadata, where they are scheduled (nodeSelector,affinity,tolerations,topologySpreadConstraints,schedulerName,priorityClassName) and their settings (serviceAccountName,automountServiceAccountToken,hostNetwork,hostUsers,hostAliases,dnsPolicy,dnsConfig,enableServiceLinks,terminationGracePeriodSeconds); - the StatefulSet:
serviceName,podManagementPolicy,updateStrategy,minReadySeconds,persistentVolumeClaimRetentionPolicy; - Alertmanager itself:
configSecret,alertmanagerConfiguration,alertmanagerConfigSelector,alertmanagerConfigNamespaceSelector,alertmanagerConfigMatcherStrategy,secrets,configMaps,retention,logLevel,logFormat,externalUrl,routePrefix,listenLocal,portName,web,limits,enableFeatures,additionalArgs,paused; - its cluster:
additionalPeers,clusterAdvertiseAddress,clusterGossipInterval,clusterLabel,clusterPushpullInterval,clusterPeerTimeout,clusterPeerName,clusterTLS,forceEnableClusterMode.
The type’s comments say that an entry sharing its name with a container the operator generates is merged into it. For an Alertmanager the operator generates
alertmanagerandconfig-reloaderundercontainers, andinit-config-reloaderunderinitContainers(makeStatefulSetSpecinpkg/alertmanager/statefulset.goof prometheus-operator v0.94.1, the version the linked module is cut from); it generates nothanos-sidecar, so an entry of that name is a container of its own. Of the three, onlyalertmanagertakes its image from the spec; the two reloaders run the image of the operator’s own configuration unless an entry patches them. Launcher holds every entry the same way, patch or not.additionalArgsis passed to the alertmanager container as written; only an argument’s name is read, to refuse one that names a flag the operator generates (below). An argument can still change what the other fields configure.Not authorable:
baseImage,tagandsha. Each is refused when not empty, beside animagetoo, with or without a policy (tag: not authorable: the Prometheus operator deprecates the field, and composes the image it yields outside what the object states; use image). The three are deprecated upstream, and the image they yield is composed in operator code outside the linked module, so the kind cannot say which image runs and has nothing to hold to the allowed registries or the tag rule. They are not in the schema, so a caller that validates the properties against it first, askurel builddoes, refuses each as an unsupported field whatever its value, an empty one included. A caller that converts the component without that validation gets the text above, and there an authored empty string is the object an absent one is, and builds.Required follows the rule of those four kinds: a field the API requires that the Go type writes whether or not it was authored. No top-level field is one. Of what is authored below them (
clusterTLS.server: required (…),hostAliases[1].hostnames: required (…)):- an additional argument’s
name, a DNS option’sname, a host alias’sipandhostnames; - the
serverand theclientof aclusterTLS; - the
typeof anupdateStrategy; - under
alertmanagerConfiguration.global: thehostand theportofsmtp.smartHost, and theclientId,clientSecretandtokenUrlofhttpConfig.oauth2; - the
keyandoperatorof a match expression, in every label selector the spec holds (affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution[0].labelSelector.matchExpressions[0].operator: required (…); the selectors are listed under “A label selector’s match expressions”). Presence only, as on the monitors.
TestMonitoringKinds_RequiredMatchMarkersholds the list to the markers of the linked module’s source. The module ships no CRD, so there is no schema to hold it to, and what the API server requires beyond the markers is not known here. Not refused: a required field of a Kubernetes type the spec embeds (a container’sname, thekeyof a Secret key reference), which is emitted empty; and two fields of a listed container or init container and two of a listed volume that upstream marks required and the type leaves out when empty (theactionof a container restart rule and theoperatorof its exit codes, thesignerNameandkeyTypeof a pod certificate source), as on the pod kind.An authored
0or""the type cannot carry is refused. As on the pod kinds, on a listed container, init container or volume: atimeoutSeconds,periodSeconds,successThresholdorfailureThresholdwritten as0on a liveness, readiness or startup probe (containers[0].readinessProbe.periodSeconds: 0 cannot be carried by the Prometheus operator API types (…)); an empty string the API server defaults, such as a container’simagePullPolicy, a port’sprotocol, anhttpGetpathorscheme, or a volume source’s default; and, underhostNetwork: true, a port’shostPortwritten as0, which the API server would set to the port’scontainerPort. The refusal holds for an entry that patches one of the operator’s own containers too: the zero is left out there as anywhere, and what then applies is the operator’s value for that container, not the authored one. As on the other kinds of the operator’s API: an emptyportName,retentionoralertmanagerConfigMatcherStrategy.type, which the CRD defaults toweb,120handOnNamespace(portName: "" cannot be carried by the Prometheus operator API types (…)). An emptyschedulerNameorimagePullPolicyis refused too: the operator copies each one unchanged to the pods, the second to its own containers, where the API server defaults an empty one.TestMonitoringWorkloadKinds_DefaultedZerosderives the list. It reads the type, the default markers of the operator’s source, the field comments of the Kubernetes types, and the API server’s defaulting code for their strings. It then shows each field refused on a document.TestMonitoringWorkloadKinds_HostNetworkHostPortshows thehostPortrefusal. An authoredhostNetwork: falseis left out, and the API reads an absent one asfalse.What the operator builds from the spec is checked where the API or the operator would break it. The kind leaves to the API what the CRD’s own schema refuses when the Alertmanager is applied (a
retentionof1.5hor1d, for one), which shows at once; it refuses what the CRD admits but the operator or the API then refuses on the StatefulSet or the pods built from it, which would fail late and out of sight. Each is refused, under any policy and none, atpkg/alertmanager/statefulset.goof prometheus-operator v0.94.1:versionunset whereimage, or a listedalertmanagerentry, names the image: the operator chooses the container’s flags byversion, and by the deployed operator’s default where none is, which a build cannot know and need not be the version the image runs (version: required where image, or an entry of containers named alertmanager, names the image); and aversionthe operator fails the reconcile on: onesemver.ParseTolerantcannot parse, one under 0.15.0, or one of a major version above 0 (operator.go:902-909).- a field the operator reads only from some Alertmanager version on, where
versionis below that version. An unsetversion(which is required only where the image is named) is judged against the operator release launcher vendors (v0.94.1, default v0.34.0): the deployed operator’s default is unknown at build time, and at v0.34.0 no field below is refused. Below its version the operator leaves the flag off the container, the TLS or HTTP configuration out of the web server, the gossip TLS configuration off the cluster, or the field out of the global configuration, or refuses the AlertmanagerConfig objectnamenames at reconcile (clusterPeerName: read by the Prometheus operator only for Alertmanager 0.30.0 and later; at version v0.28.1 it does not pass it to Alertmanager, and the pods run without it; raise version to 0.30.0 or later, or leave clusterPeerName unset). Pods:logFormatother thanlogfmt(0.16.0),web.getConcurrencyandweb.timeout(0.17.0),web.tlsConfigandweb.httpConfig(0.22.0),clusterTLS(0.24.0),clusterLabel(0.26.0),enableFeatures(0.27.0),limits.maxSilencesandlimits.maxPerSilenceBytes(0.28.0),clusterPeerName(0.30.0). UnderalertmanagerConfiguration.global:httpConfig.authorization,httpConfig.oauth2andhttpConfig.followRedirects(0.22.0);telegram(0.24.0);webex,httpConfig.enableHttp2,httpConfig.tlsConfig.minVersionandmaxVersion, andhttpConfig.oauth2.proxyUrl,noProxyandproxyConnectHeader(0.25.0);httpConfig.proxyFromEnvironmentset to true,httpConfig.noProxyandhttpConfig.proxyConnectHeader(0.26.0);jira,rocketChatandsmtp.tlsConfig(0.28.0);smtp.forceImplicitTLS(0.31.0);mattermost(0.32.0).TestAlertmanagerVersionGates_MatchVendoredSourceparses every version comparison of the operator’s packagepkg/alertmanager(every Go file of it, its tests aside; at v0.94.1 the comparisons are instatefulset.go,amcfg.goandoperator.go) and holds each to this list, or to the reason it gates no field of this kind (the configuration of a receiver, a route or a configSecret Secret); the files, with the operator’spkg/operator/defaults.go, are vendored unmodified undertestdata/upstream/prometheus-operator, with the project’s LICENSE and NOTICE and a SOURCE file naming each file’s git blob id, andmise run vendor-prometheus-operator-gates <tag>re-fetches them for the taggo.modlinks. - an
enableFeaturesAlertmanager exits on at startup, atversion(unset, the operator’s default v0.34.0). The operator passes the list from 0.27.0 on, joined with,, as--enable-feature(statefulset.go:311-315), and Alertmanager splits it on,and exits on an element that is not a feature flag of its version (featurecontrol/featurecontrol.go:135-180,cmd/alertmanager/main.go:104-108at v0.34.0). So an element with a,in it is two, an empty element is refused (["classic-mode", ""]), andclassic-modewithutf8-strict-modeis refused. A list of one empty element is refused at any version: the operator writes its empty value as--enable-featurealone (pkg/operator/argument.go:69-75at v0.94.1), which Alertmanager’s flag parser refuses (kingpin v2.4.0flags.go:126-130) before it exits; leaveenableFeaturesunset for no feature. The joined list is read as the kubelet passes it, which reads$$as$and leaves$(without its closing parenthesis as written; one that refers to an environment variable, as$(AM_FEATURE), is not refused, since the kubelet expands it to a value the component cannot see. The names, by minor:classic-mode,receiver-name-in-metricsandutf8-strict-modefrom 0.27;auto-gomaxprocsandauto-gomemlimitfrom 0.28;alert-names-in-metricsfrom 0.31;event-recorderandgroup-key-in-metricsfrom 0.33, which dropsauto-gomaxprocs(enableFeatures: not a feature flag of Alertmanager 0.33, at version v0.33.0: …; it takes alert-names-in-metrics, …). A prerelease is judged at its minor. Above 0.34 the names are not known, and none is refused.TestAlertmanagerFeatureFlags_MatchVendoredSourceholds the names, andNewFlagsstatement by statement, tofeaturecontrol.goof each minor’s latest release, vendored unmodified undertestdata/upstream/alertmanagerwith the project’s LICENSE, each release’s NOTICE and a SOURCE file naming each file’s git blob id;mise run vendor-alertmanager-features 0.27 0.34re-fetches them. - a
portNamethe API refuses where the operator writes it: not an IANA service name (at most 15 characters, lower-case letters, digits and-); unlesslistenLocalis set,mesh-tcp/mesh-udp, the container ports the operator adds beside the web port; and unlessserviceNamenames a Service of the author’s,tcp-mesh/udp-mesh, the ports of the governing Service the operator creates (statefulset.go:223-250, :483-502). - a negative
replicas, which the operator runs as 0. storage.volumeClaimTemplate.metadata.namebesidestorage.emptyDirorstorage.ephemeral, other thanalertmanager-<name>-db: the operator mounts the data volume under that name and creates it asalertmanager-<name>-db, so the pods mount a volume that does not exist.- a storage arm in use whose claim the API refuses: the claim template arm
(which an unset arm selects, an empty
storageincluded) withoutspec.resources.requests.storage(unset or empty access modes, which are not serialized, the operator writes asReadWriteOnce); the ephemeral arm without a claim template, its access modes or its storage request, which the operator uses as written; and on either, a storage request of0or less, as the API requires a positive one. - on the claim template arm, a claim template name that is not a DNS-1123 label, which the operator names the data volume with, or that names a volume the operator adds, which the StatefulSet controller then replaces with the claim.
- an entry of
volumesnamed as a volume the operator adds:config-volume,tls-assets,config-out,web-config,cluster-tls-config,notification-templateswherealertmanagerConfiguration.templatesis set, the name the operator derives for each ofsecretsandconfigMaps(secret-<name>,configmap-<name>), and the data volume’s: besidestorage.emptyDirorstorage.ephemerala second volume of one name, and on the claim template arm one the StatefulSet controller replaces with the claim (volumes[0] "config-volume": the name is a volume the Prometheus operator adds to every Alertmanager's pods; name the volume otherwise).web-configandcluster-tls-configare refused whateverversionnames, though the operator adds them only for Alertmanager 0.22.0 and 0.24.0 on, so that raisingversiondoes not turn a volume of the author’s into a clash. The volumes of the web and cluster TLS credentials are not checked: the operator names each after the credential’s source with a hash appended, which the kind does not derive, so an entry under one of those names is left to the API to refuse. Two entries ofsecrets, or ofconfigMaps, that the operator’s naming gives one volume name (alerts.configandalerts-configboth namesecret-alerts-config) are refused as well: the operator adds a volume for each (statefulset.go:638-690). So is an entry whose volume name, cut to 63 characters, ends in-: the operator checks the name after the cut and fails to build the pods (ResourceNamer.DNS1123Label, statefulset.go:640-643). - an entry of
volumeMountsat a path the operator mounts a volume at in the alertmanager container, as the API refuses two mounts at one path:/alertmanager,/etc/alertmanager/config,config_outandcerts, the web and cluster TLS configuration files whateverversionnames,/etc/alertmanager/templateswherealertmanagerConfiguration.templatesis set, and/etc/alertmanager/secrets/<name>and/etc/alertmanager/configmaps/<name>for each entry ofsecretsandconfigMaps(:531-556, :575-692). The mounts of the web and cluster TLS credentials are left to the API, as their volumes are. - an entry of
additionalArgswhose name, or that name withno-added or taken away, is a flag the operator generates for the spec: it then fails to build the pods (BuildArgs,pkg/operator/argument.go:26-79). The flags areconfig.file,storage.path,data.retention,web.listen-address,web.route-prefix,cluster.reconnect-timeout,cluster.listen-address(writtencluster.listen-address=on one replica withoutforceEnableClusterMode),cluster.peerwhere a replica oradditionalPeersis, and the flag of each field set amongexternalUrl,enableFeatures,web.getConcurrency,web.timeout,limits,logLevelother thaninfo,logFormatother thanlogfmt,clusterAdvertiseAddress, the three cluster durations andclusterTLS(statefulset.go:289-508, :700-748), andcluster.peer-name,cluster.labelandweb.config.file. A flag the operator generates only from some version on is refused from that version on (cluster.peer-name0.30.0,enable-feature0.27.0,cluster.label0.26.0,cluster.tls-config0.24.0,web.config.file0.22.0,web.get-concurrencyandweb.timeout0.17.0,log.format0.16.0, thelimitsflags 0.28.0), and always whereversionis unset: the operator then runs its default version, v0.34.0 (DefaultAlertmanagerVersion, pkg/operator/defaults.go:25), above every one of them, and no flag is generated only up to a version. Notdispatch.start-delay: the operator leaves its own out where an argument names it. - an entry of
alertmanagerConfiguration.templateswhose key an earlier entry names,configMaporsecret: the operator projects each key at the path of its name and skips a later entry of a key it has projected (statefulset.go:575-620), so that template would not be loaded. - a
web.tlsConfig,clusterTLS.serverorclusterTLS.clientthe operator’s own validation refuses (a certificate and a key, each named once, and a certificate for the client; webconfig.New, clustertlsconfig.New): it then builds no pods. dnsPolicy: NonewithoutdnsConfig.nameservers, and a pod-levelsecurityContext.windowsOptions.hostProcess: truewithouthostNetwork: true: the API refuses the pods the operator copies them into.- a negative request or limit in
resources, as merged, or in a listed container: the CRD’s quantity pattern admits a sign, and the API refuses the container the operator builds with it. - a name the operator’s objects cannot be named after: the data volume
alertmanager-<name>-db, unless a claim template’s name names it, and the hostnamealertmanager-<name>-<replicas-1>of the last pod must each be a DNS-1123 label; and the StatefulSetalertmanager-<name>must be at most 52 characters: the StatefulSet controller labels each of its podscontroller-revision-hashwith that name, a-and a hash of up to 10 characters, a label value the API refuses beyond 63 characters. So a name has no dot and at most 39 characters. The hostname and the label do not apply wherereplicasis 0, with no pod to name or label. The refusal names the component, orobjectNamewhere that set the name.
The API’s expression rules are not checked. The types the spec reaches state one: an
updateStrategywith arollingUpdatemust have the typeRollingUpdate. The linked module ships no CRD, so there is no rule text to run through the validator the other kinds use: the rule is listed (alertmanagerRulesLeft) and left to the API server, andTestMonitoringWorkloadKinds_RulesListedfails when a dependency bump adds, drops or rewords a rule on a reached type. The API’s other value rules (formats, enumerations, minima) are left to it as well.What the type writes unauthored. Every Alertmanager carries
resources: {}andalertmanagerConfigMatcherStrategy: {}; the API defaults thetypeof the second toOnNamespace. An authoredstoragecarries avolumeClaimTemplatewhether or not one was authored, withmetadata: {}andstatus: {}beside itsspec; upstream documents that anemptyDiror anephemeraltakes precedence over it.With or without a policy, an authored
image(unless a patch ofalertmanagernames an image, which replaces it in the container the operator builds, so only the patch’s is run and held), the authored image of a listed container or init container, and thereferenceof an image volume are held to the tag rule (ValidateImageRef:image: image "…" rejected: :latest tag not allowed), and a resource block’s request may not exceed its limit (resources: cpu: request 2 must not exceed limit 1,containers[0] "proxy": resources: memory: request 2Gi must not exceed limit 1Gi). The alertmanager container runs withresourcesas the operator fills it, an unset memory request as 200Mi whatever the limit, and with the requests and limits of a listedalertmanagerentry merged over that block key by key (makeStatefulSetSpec, same source); the checks hold that merged block with the 200Mi filled where it names no memory request, and not the patch’s block alone. So hugepages need no authored cpu or memory beside them, the filled request naming memory, and an extended resource the patch requests meets a limit named inresources. A memory limit under 200Mi with no memory request in either is refused, as the API would refuse the pods (resources: memory: the unset request the Prometheus operator fills as 200Mi must not exceed limit 100Mi; …; with a patch, the path isresources with containers[0] "alertmanager" merged over it), while a request the patch names replaces the 200Mi and is held with the limit ofresources. An entry named for a container the operator generates, in the list the operator generates it in, is merged into it, so such a patch may name no image; any other listed entry that names none is refused, as no pod could run it (containers[1] "proxy": names no image, and the Prometheus operator generates no container of that name to merge it into; …). A name listed twice incontainersorinitContainersis refused (containers[1] "config-reloader": the name is listed already at containers[0], …): the operator keeps only the last entry of a name (MergePatchContainers,pkg/k8s/merge.goat v0.94.1), so the policy would hold an entry that never runs. A name shared by an init container and a container of the pods, generated or listed, is refused, as the API requires the two lists’ names to be unique together (containers[0] "init-config-reloader": the name is also that of the init container the Prometheus operator generates, …). A patch’s ports are merged into the ports the operator gives the generated container by number, as a strategic merge does (MergePatchContainers, same source): in the patch’s order, each port is merged into the first port of its number, the operator’s or one an earlier port of the patch added, and replaces the name and protocol it names; a port of a number no port has is added. Where the operator gives the container no port (config-reloader underlistenLocal), the merge takes the patch’s ports as listed. The operator’s ports, in its order, are the web port underportNameat 9093/TCP and the config-reloader’sreloader-webat 8080/TCP unlesslistenLocalis set,mesh-tcpat 9094/TCP thenmesh-udpat 9094/UDP, and the init-config-reloader’sreloader-initat 8081/TCP. A container whose ports, merged so, or as a listed container that is not a patch lists them, name two ports alike is refused, as the API refuses two ports of one name (containers[0] "alertmanager": ports[1] "mesh-udp" at 9000/UDP: the Prometheus operator merges the patch's ports into the container's by number, which leaves another port of that name, the Prometheus operator's port at 9094/UDP, …; a port at 9094 renamesmesh-tcp, nevermesh-udp). A patch may rename a generated port and reuse its name, and a later port of the patch merges into one an earlier port added. Twovolumesof one name are refused, as the API refuses the pods (volumes[1] "scratch": the name is listed already at volumes[0], …), and so is aserviceNamethat is not a DNS-1035 label (a leading digit included), the rule of every Service name in this package: the API refuses a Service of another name before Kubernetes 1.36 (by default), and the operator fails the reconcile where it finds no governing Service of the name (serviceName: "1alerts" is not a valid Service name, which must be a DNS-1035 label: …). Kubernetes 1.36 and later admit such a name (go-kure/launcher#959).retention,clusterGossipInterval,clusterPushpullIntervalandclusterPeerTimeoutare refused where they parse as a duration of 0 or less (retention: "0s" is not a positive duration: …): the operator empties such a value before it builds the StatefulSet and runs the pods as if the field were unset (discardZeroDurations, same source). A value that does not parse is left to the API’s pattern. A nonemptyexternalUrlthat Alertmanager exits on at startup is refused too, though the operator and the API accept it: the operator passes it unchanged as--web.external-url, and Alertmanager exits where Go’snet/urlcannot parse it, and, from v0.19.0 on, where its scheme is nothttporhttps(externalUrl: not a URL of scheme http or https: …;cmd/alertmanager/main.goof v0.28.1,app/url.goof v0.34.0). The scheme is held whereversionis unset or names v0.19.0 or later, its prereleasev0.19.0-rc.0included; an earlier version takes any scheme. Neither message names the value. A nonemptyclusterAdvertiseAddressthat is not an IP address and a numeric port written ashost:portis refused where the cluster runs, atreplicasother than 1 (0 included: the object configures the cluster for the pods a scale-up starts) or withforceEnableClusterMode(clusterAdvertiseAddress: not an IP address and a numeric port, …): the operator passes it unchanged as--cluster.advertise-address, and Alertmanager, from v0.15.0 to v0.34.0, splits it withnet.SplitHostPort, reads the port withstrconv.Atoiand gives the host to memberlist, which takes only an IP address; any failure ends the cluster’s creation, and Alertmanager exits (cluster/cluster.go:162-171andapp/app.go:214-234of v0.34.0). The host is judged an IP address in full only where Alertmanager is built with Go 1.23 or later, whosenet.ParseIPthe check uses: from v0.28.0 on, its prereleasev0.28.0-rc.0included (.promu.yml), and whereversionis unset. This is a limit: older Alertmanager versions parse IP addresses with older Go rules, which this check does not model (they take some leading zeros Go 1.23 refuses, as010.0.0.1orfd00::00001), so before v0.28.0 onlyhost:port, a numeric port and a host of the characters an IP address has (hex digits,.and:) are held. A host with any other character, as the host namealerts.example.com, is no IP address for thenet.ParseIPof any Go and is refused at every version; one of those characters only, ascafe.dead, is not refused before v0.28.0. An empty host, as in:9094, is not refused: Alertmanager then works the address out itself; nor is a port number no socket has, which memberlist truncates. The port is read asstrconv.Atoireads it on a 32-bit image (the armv7 one), so one above 2147483647 is refused whatever the architecture. The value is read as the kubelet passes it, which reads$$as$and leaves$(without its closing parenthesis as written; one that refers to an environment variable, as[$(POD_IP)]:9094, is not refused, since the kubelet expands it to a value the component cannot see (expand.goof Kubernetes v1.35.0). With the cluster off the value is not read. The message does not name the value.Policy.
ApplyPolicyrefuses or passes; it writes nothing, and without a policy the same component builds. Refused, each with the class a workload kind’s refusal has:imagewhere no patch ofalertmanagerreplaces it, the image of a listed container or init container (a patch ofalertmanagerincluded), and an image volume’s reference, outside the allowed registries (oam.RefusalRegistry);- under a policy that lists allowed registries, an image of a container
the operator generates that the spec leaves to it (
oam.RefusalRegistry): thealertmanagercontainer’s where neitherimagenor a patch of it names one, andconfig-reloader’s orinit-config-reloader’s where no patch names one. See below; replicasover the replica maximum, an unset one counted as the 1 the operator runs (replicas 1 exceeds enforced maximum 0,oam.RefusalReplicaMaximum);- the cpu or memory of
resources, and of a listed container, over the maxima (oam.RefusalResourceMaximum), on a patch of one of the operator’s containers as on any other; for the alertmanager container, the block it runs with, a patch merged overresources. Where that block names no memory request, the 200Mi the operator requests is held in its place (resources, whose unset memory request the Prometheus operator fills as 200Mi: memory request "200Mi" exceeds enforced maximum "128Mi"); - the storage a claim requests over the storage maximum
(
oam.RefusalStorageMaximum): ofstorage, the arm the operator uses, readingemptyDir, thenephemeral, thenvolumeClaimTemplateand taking the first that is set, so a claim template after an arm in use is not held, as no claim is made from it; and a generic ephemeral volume undervolumes; hostNetwork: trueunder a policy that does not allow the host network (oam.RefusalHostNamespace); the spec has no field for the host’s process or IPC namespace;- a hostPath volume under
volumes(oam.RefusalHostPath); - a privileged listed container, and a pod-level
securityContext.windowsOptions.hostProcess, under a policy that does not allow privileged containers (oam.RefusalPrivileged), and a capability the policy does not allow (oam.RefusalContainerCapability).
An image the operator chooses is refused under a registry allowlist. Where the spec names no image for a container the operator generates, the operator chooses the one it runs, and no registry allowlist reaches that choice, so a policy with a non-empty
AllowedRegistriesrefuses it (image: unset, so the Prometheus operator chooses the image the pods run, …;the image of the config-reloader container (containers): unset, …) and the author names an image from one of the listed registries:imageor a patch ofalertmanagerfor the first, a patch of each reloader for the other two. Without such a list, it builds.The operator’s own values are held, not written. Where the spec leaves
replicasor the alertmanager container’s memory request unset, the operator fills 1 and 200Mi before it builds the StatefulSet (makeStatefulSet, same source); the policy holds those values as if authored, and the emitted object still leaves both unset.TestMonitoringWorkloadKinds_PodFieldsHeldOrListedderives, from the type, the fields of the spec that carry the name of a field of a pod spec, a container or a StatefulSet’s spec, the storage block, and any field that holds object metadata, and holds each to one of three answers: held to the policy, with a refusal shown on a document (the nine above:image,replicas,resources,storage,volumes,containers,initContainers,securityContext,hostNetwork); read by the wrapper (podMetadata, below); or stated with the reason it is neither. A dependency bump that adds such a field fails there, naming it. A field that shapes the pods under a name of the operator’s own is not derived:secretsandconfigMapsmount the named Secrets and ConfigMaps of the namespace, read-only, into the alertmanager and config-reloader containers (makeStatefulSetSpec, same source), and fields such asretentionandlogLevelbecome arguments. The policy has no dimension for a Secret or ConfigMap volume or for an argument, so none of them is held. Not held:- An image the operator chooses, under a policy without allowed
registries, as stated above.
versiondoes not name an image. - What the operator adds on its own, beyond the images, the replica
count and the memory request above: the resources of a reloader no
entry patches, the arguments it derives, the governing Service it
creates where
serviceNameis unset. None of it is in the object. - No policy default is filled. An unauthored
replicas,resourcesorstoragestays unauthored under a policy that states a replica, a resource or a storage default: the operator, not launcher, decides what the omission means, and its own value is what the policy holds. - The size limit of an
emptyDir, understorageorvolumes, here as on every kind. - Fields the environment policy has no dimension for, on a pod kind
either: where and in which order the pods are scheduled, how the
StatefulSet rolls and what becomes of its claims,
serviceAccountName(launcher creates no ServiceAccount for it and does not check that one exists),automountServiceAccountToken, the DNS settings,hostAliases,hostUsers,terminationGracePeriodSeconds,imagePullPolicy,imagePullSecretsandvolumeMounts.
podMetadatais the author’s, and nothing is added to it. The operator copies its labels and annotations onto the pods. The wrapper every component’s objects pass reads it as it reads a workload’s pod template: a key the consumer reserved (ReservedMetadataKeys) is refused, and so is the component label key with another value than the component’s. It writes nothing there. So the operator’s pods carry the component label only if the author writes the component’s own value intopodMetadata.labels; otherwise the NetworkPolicies generated for the component do not select them. The Alertmanager object itself carries the component label, as every object a component owns. The operator sets five labels of its own on the pods (alertmanager,app.kubernetes.io/instance,app.kubernetes.io/managed-by,app.kubernetes.io/name,app.kubernetes.io/version) and the annotationkubectl.kubernetes.io/default-container. A value authored forapp.kubernetes.io/versionreplaces the operator’s, which it copiespodMetadata.labelsover; the other four labels and the annotation it writes after them, so an authored value does not replace those (statefulset.go:446-460). Launcher does not refuse such a key.The metadata of a claim template takes no component label. The labels and annotations of
storage.volumeClaimTemplateare held to the reserved metadata keys: the operator copies them onto the volume claim template of its StatefulSet, and so onto every claim the StatefulSet controller creates (volume claim template label "…", go-kure/launcher#957). Besidestorage.emptyDirorstorage.ephemeralthe operator makes no claim from it, and they are not read. Those ofstorage.ephemeral.volumeClaimTemplateand of a generic ephemeral volume undervolumesgo onto a claim of a pod and are not read. All three are written as authored.No field holds a credential in the clear, and none is checked. Every credential of the spec is the key of a Secret (a
weborclusterTLSkey, an SMTP password, an OAuth2 client secret), andconfigSecret,secretsandimagePullSecretsname Secrets whose content is not in the object.TestMonitoringWorkloadKinds_CredentialsHeldOrListedderives the fields of the operator’s types whose name suggests a credential and holds each to a stated reason it holds none. Free text that could hold one is written as authored, under a policy that forbids explicit secrets too:additionalArgs,externalUrl, and theenvof a listed container, as on a pod kind.Not covered. Whether what the object refers to exists (the configuration Secret, a mounted Secret or ConfigMap, the ServiceAccount, a governing Service named by
serviceName, the AlertmanagerConfig objects the selectors match). Upstream documents that, without the configuration Secret or itsalertmanager.yamlkey, the operator provisions a configuration that drops alert notifications. Launcher points no Prometheus at the Alertmanager. The object’s status is the operator’s and is not written.- the image:
thanosruler (go-kure/launcher#790) is the kind-named projection of a ThanosRuler of the same API. It is built as
alertmanageris, onpolicyHeldKindwith the same policy check (monitoring_workload.go), and emits that one object in the build namespace, named after the component unlessobjectNamenames it; the handler declares the object as namespaced and adds no label and no annotation. Launcher emits no pod, no StatefulSet and no Service for it: the operator builds a StatefulSet from the object and runs the pods. What is said ofalertmanagerabove holds here unless this entry says otherwise: no capability is required and nothing gates the kind; an authored0the type cannot carry is refused on a listed container, and so is an emptyportName,evaluationInterval,retentionoractionof aremoteWriteentry’swriteRelabelConfigsrule, which the CRD defaults toweb,15s,24handreplace(evaluationInterval: "" cannot be carried by the Prometheus operator API types (…)), while a string default of the Kubernetes pod types is not refused; the tag rule and the request-over-limit check hold with or without a policy, and so does the refusal of a memory limit under the 200Mi the operator requests whereresourcesnames none; no policy default is filled; an image the operator chooses is refused under a registry allowlist, and the operator’s own replica count and memory request are held, not written (both below);podMetadatais read for reserved keys and takes no label; a claim template’s metadata is not read. The operator’s behaviour is read from its source at prometheus-operator v0.94.1 (pkg/thanos/statefulset.go). The zero-duration rule ofalertmanagerhas no counterpart: the operator passes a set duration through as written.One field is renamed: the spec’s
labelsis theexternalLabelsproperty. Every kind component’slabelsproperty is its object’s own labels (see “The object’s labels and annotations” below): the engine reads it before the handler sees it, so the spec’s field cannot be authored under its own name. These are the ThanosRuler’s Prometheus external labels, a map of label names to values (upstream: the operator always adds athanos_ruler_replicalabel with the pod’s name), not Kubernetes labels: the reserved-key and component-label checks oflabelsdo not apply to them, and nothing is added to them. Authored together,labelslands on the object’s metadata andexternalLabelson itsspec.labels.externalLabelsis decoded as strictly as the spec, under the same null contract, and an error namesexternalLabels, not the spec field. A key that names the spec’s field in another spelling (Labels) is refused (Labels: not a thanosruler property: the ThanosRuler's external labels are the externalLabels property, …): the strict decode matches field names case-insensitively and would otherwise carry it into the spec unpublished. The rename is the kind’s alone; the engine’slabelskeeps its meaning.TestCoreKindSchemas_CoverSpecrecords it as a mapping (externalLabelsforlabels), so a second renamed field fails there until it is listed.Authored. The properties are the top-level json fields of
ThanosRulerSpec, decoded strictly at every depth,labelsrenamed as above: the image (image,version,imagePullPolicy,imagePullSecrets); the pods, as onalertmanagerlesshostNetwork, which this spec does not have; the StatefulSet (serviceName,podManagementPolicy,updateStrategy,minReadySeconds); and the ruler itself: what it queries and where it sends alerts (queryEndpoints,queryConfig,alertmanagersUrl,alertmanagersConfig,alertQueryUrl,alertDropLabels,alertRelabelConfigs,alertRelabelConfigFile), which rules it evaluates and how (ruleSelector,ruleNamespaceSelector,enforcedNamespaceLabel,excludedFromEnforcement,prometheusRulesExcludedFromEnforce,evaluationInterval,resendDelay,ruleOutageTolerance,ruleQueryOffset,ruleConcurrentEval,ruleGracePeriod), where its data goes (retention,objectStorageConfig,objectStorageConfigFile,remoteWrite), its server (listenLocal,portName,routePrefix,externalPrefix,grpcServerTlsConfig,web),tracingConfig,tracingConfigFile,logLevel,logFormat,enableFeatures,additionalArgsandpaused.additionalArgsis passed to the thanos-ruler container as written and is not read; upstream documents that an argument the operator already sets, or an invalid one, fails the reconciliation. An entry ofcontainersnamedthanos-rulerorconfig-reloaderis a patch of the container the operator generates under that name (makeStatefulSetSpec, merged byk8s.MergePatchContainers), so such a patch may name no image; any other entry ofcontainersthat names none is refused (containers[0] "proxy": names no image, and the Prometheus operator generates no container of that name to merge it into; name an image, or the container it patches (thanos-ruler, config-reloader)). The operator generates no init container and adds each entry ofinitContainersas written, so every one must name an image (initContainers[0] "prepare": names no image, and the Prometheus operator merges no entry of initContainers into a container of its own; name an image). The thanos-ruler container runs withresourcesas the operator fills it, an unset memory request as 200Mi, and with the requests and limits of a listedthanos-rulerentry merged over that block key by key (makeStatefulSetandmakeStatefulSetSpec,pkg/thanos/statefulset.go:62-67,:481and:494); the checks hold that merged block with the 200Mi filled where it names no memory request, and not the patch’s block alone, as onalertmanager. A memory limit under 200Mi with no memory request in either is refused, as the API would refuse the pods (resources: memory: the unset request the Prometheus operator fills as 200Mi must not exceed limit 100Mi; …; with a patch, the path isresources with containers[0] "thanos-ruler" merged over it), while a request the patch names replaces the 200Mi and is held with the limit ofresources. An image athanos-rulerentry names replacesimage, soimageis then not held and the entry’s is. Nothing is refused as not authorable.Required. No top-level field is one. Of what is authored below them: an additional argument’s
name, a DNS option’sname, a host alias’sipandhostnames, thetypeof anupdateStrategy; thenamespaceandresourceof anexcludedFromEnforcemententry, theruleNamespaceandruleNameof aprometheusRulesExcludedFromEnforceentry; and of aremoteWriteentry itsurl, theclientId,clientSecretandtokenUrlof itsoauth2, and theclientId,tenantId(and, underazureAd.oauth,clientSecret) of its Azure AD settings; thekeyandoperatorof a match expression, in every label selector the spec holds, presence only, as on alertmanager.TestMonitoringKinds_RequiredMatchMarkersholds the list to the markers of the linked module’s source, which ships no CRD.What the operator builds from the spec is checked where the API would break it, as on
alertmanager, under any policy and none, atpkg/thanos/statefulset.goof prometheus-operator v0.94.1:- a
portNamethe API refuses where the operator writes it: not an IANA service name; andgrpc, the port the operator adds beside the web port on the thanos-ruler container unlesslistenLocalis set, and on the governing Service it creates unlessserviceNamenames one of the author’s (:216-232, :545-583;portName: "grpc" is the name of a port the Prometheus operator adds to the thanos-ruler container, …). The operator writesportNamenowhere else, as it gives the ruler no probe, so on the container it is judged as the pods run the web port: athanos-rulerpatch that names port 10902 otherwise leaves it out of the container, and one that renamesgrpcfrees that name. The Service’s half is held whatever a patch says. A patch ofthanos-rulerorconfig-reloaderis merged by port number into the ports the operator gives them (grpcat 10901/TCP, the web port at 10902/TCP andreloader-webat 8080/TCP, the last two unlesslistenLocalis set), and a port name that leaves two of one name is refused, as onalertmanager. AserviceNamemust be a DNS-1035 label. - a negative
replicas: the operator copies the count into the StatefulSet as written (:507), which the API refuses. - the storage rules of
alertmanager(:92-133, :441-450): a claim of the arm in use the API refuses, a claim template name besideemptyDirorephemeralother thanthanos-ruler-<name>-data, and on the claim template arm one that is not a DNS-1123 label or that names a volume the operator adds. - an entry of
volumesnamed as a volume the operator adds:tls-assets,web-config(whateverversionnames, as onalertmanager),remote-write-config, the volume of each Secret key the spec sets (query-config,alertmanager-config, andobjstorage-config,tracing-configandalertrelabel-configwhere the file field that takes precedence is unset),thanos-ruler-<name>-rulefiles-<n>of the rule ConfigMaps, of which the operator makes as many as the selected rules need (pkg/thanos/rules.go:86-104,pkg/operator/rules.go:363-365), and the data volume’s (:135, :243-297, :377-398, :452-469;volumes[0] "tls-assets": the name is a volume the Prometheus operator adds to every ThanosRuler's pods; …). The web TLS credentials’ volumes are left to the API. - an entry of
volumeMountsat a path the operator mounts a volume at in the thanos-ruler container:/thanos/data,/etc/thanos/certs,/etc/thanos/web_config/web-config.yaml,/etc/thanos/config/<volume>of each Secret key’s volume above, and/etc/thanos/rules/thanos-ruler-<name>-rulefiles-<n>(:297-302, :394, :441-471, :601-626). dnsPolicy: NonewithoutdnsConfig.nameservers: the API refuses the pods the operator copies them into (:539-540).- a name the operator’s objects cannot be named after: the data volume
thanos-ruler-<name>-data, unless a claim template’s name names it, and the first rule ConfigMap’s volumethanos-ruler-<name>-rulefiles-0must each be a DNS-1123 label, so a name has no dot and at most 38 characters. Each pod’s hostname,thanos-ruler-<name>-<ordinal>, is no longer than that volume’s name at any replica count. The refusal names the component, orobjectNamewhere that set the name.
Not checked of what the operator refuses: a ThanosRuler with neither
queryConfignorqueryEndpoints(:141-143), aversionit cannot parse (:145-149), an entry ofadditionalArgsnaming a flag it generates (BuildArgs, :352), and aweb.tlsConfigits validation refuses (webconfig.New, :383-386). Each fails the reconcile, and is left to the operator.The group of an excluded object is written. An
excludedFromEnforcemententry that leavesgroupout (or writes itnull) is written withmonitoring.coreos.com. The Go type writes the field whether or not it was authored, and the API allows only that one value, which it defaults only where the field is absent: an empty group would be refused at apply. An authored empty group is refused by the entry’s index (excludedFromEnforcement[1].group: empty: the API admits only monitoring.coreos.com; …), not repaired. An authoredmonitoring.coreos.comis kept; another group is written as authored and left to the API server.TestMonitoringExcludedGroup_MatchesMarkersholds the written value to the default and the enum of the linked module’s source.The API’s expression rules are not checked. Two are listed (
thanosRulerRulesLeft) and left to the API server: anupdateStrategywith arollingUpdatemust have the typeRollingUpdate, and theexternalIdof a remote write entry’ssigv4needs aroleArn.What the type writes unauthored:
resources: {}, on every ThanosRuler; and, on an authoredstorage, avolumeClaimTemplatewithmetadata: {}andstatus: {}, as onalertmanager.Policy. Refused, with a workload kind’s classes:
image, where no patch ofthanos-rulernames one in its place, the image of a listed container or init container (a patch ofthanos-rulerorconfig-reloaderincluded) and an image volume’s reference outside the allowed registries; under a policy that lists allowed registries, the image of thethanos-rulercontainer where neitherimagenor a patch of it names one, and that ofconfig-reloaderwhere no patch names one;replicasover the replica maximum, an unset one counted as the 1 the StatefulSet runs; the cpu or memory ofresourcesand of a listed container over the maxima,resourceswith athanos-rulerentry’s requests and limits merged over it, and the 200Mi the operator requests held where neither names a memory request (resources, whose unset memory request the Prometheus operator fills as 200Mi: memory request "200Mi" exceeds enforced maximum "128Mi"); the storage a claim requests over the storage maximum, of thestoragearm the operator uses (emptyDir, thenephemeral, thenvolumeClaimTemplate, as onalertmanager) and of a generic ephemeral volume; a hostPath volume; a privileged listed container, a pod-levelsecurityContext.windowsOptions.hostProcessand a capability the policy does not allow. The spec has no field for the host’s network, process or IPC namespace.TestMonitoringWorkloadKinds_PodFieldsHeldOrListedholds every field of the spec that shapes the pods to a refusal shown on a document or a stated reason, as foralertmanager.An image the operator chooses is refused under a registry allowlist. Where the spec names no image for a container the operator generates, the operator chooses the one it runs, and no registry allowlist reaches that choice, so a policy with a non-empty
AllowedRegistriesrefuses it (image: unset, so the Prometheus operator chooses the image the pods run, …;the image of the config-reloader container (containers): unset, …) and the author names an image from one of the listed registries:imageor a patch ofthanos-rulerfor the first, a patch ofconfig-reloaderfor the second. The operator generatesconfig-reloaderon every ThanosRuler, as it always writes at least one rule ConfigMap (makeConfigMapsFromRules,pkg/operator/rules.go). Without such a list, it builds.The operator’s own values are held, not written. Where the spec leaves the thanos-ruler container’s memory request unset, the operator fills 200Mi before it builds the StatefulSet (
makeStatefulSet); it copies an unsetreplicasinto the StatefulSet as unset, which the API server defaults to 1. The policy holds those values as if authored, and the emitted object still leaves both unset.Credentials: one is held, the rest are stated. The deprecated
remoteWrite[].bearerTokenholds a credential in the object; under a policy that forbids explicit secrets it is refused (remoteWrite[1].bearerToken: holds a credential in the object, and the environment policy forbids explicit secrets…,oam.RefusalExplicitSecret), and without one it is written as authored. Every other credential of the spec is the key of a Secret (a remote write entry’s basic auth, authorization, OAuth2, SigV4 and Azure AD secrets) or the path of a file in the container (bearerTokenFile, akeyFile). Not held: a credential under a name that does not say so. A value ofremoteWrite[].headers, the userinfo of a URL (remoteWrite[].url,queryEndpoints,alertmanagersUrl), and free text (additionalArgs, theenvof a listed container) are written as authored, under a policy that forbids explicit secrets too.TestMonitoringWorkloadKinds_CredentialsHeldOrListedderives the fields whose name suggests a credential and holds each to the policy or a stated reason.podMetadata. As onalertmanager: the pods carry the component label only if the author writes the component’s own value intopodMetadata.labels; otherwise the NetworkPolicies generated for the component do not select them. The operator reserves four labels of its own on the pods (app.kubernetes.io/name,app.kubernetes.io/managed-by,app.kubernetes.io/instance,thanos-ruler) and the annotationkubectl.kubernetes.io/default-container; upstream documents that an authored value does not override them. Launcher does not refuse such a key.Not covered. Whether what the object refers to exists (a Secret key, the ServiceAccount, a governing Service named by
serviceName, the PrometheusRule objects the selectors match, the query and Alertmanager endpoints). What the operator adds on its own, beyond the images, the replica count and the memory request above (the resources of a config-reloader no entry patches, the arguments it derives, thethanos-ruler-operatedService), is not in the object. The object’s status is the operator’s and is not written.- a
prometheus (go-kure/launcher#790) is the kind-named projection of a Prometheus of the same API. It is built as
alertmanageris, onpolicyHeldKindwith the same policy check (monitoring_workload.go), and emits that one object in the build namespace, named after the component unlessobjectNamenames it; the handler declares the object as namespaced and adds no label and no annotation. Launcher emits no pod, no StatefulSet and no Service for it: the operator builds one StatefulSet per shard from the object and runs the pods. Unlike onalertmanager, nothing is held toversion: an image named without one is not refused, nor is aversionthe operator cannot parse, and fields the operator reads only from some Prometheus version on are not held toversion. What is said ofalertmanagerabove holds here unless this entry says otherwise: no capability is required and nothing gates the kind; on a listed container, init container or volume, an authored0or""the type cannot carry is refused, ahostPortof0underhostNetwork: trueincluded, and so is an emptyschedulerNameorimagePullPolicy, which the operator copies unchanged to the pods and to its own containers (prometheus, thanos-sidecar and both reloaders), where the API server defaults an empty one; so is an emptyportName,scrapeInterval,evaluationIntervalorthanos.blockSize, which the CRD defaults toweb,30s,30sand2h, and an emptyactionof a relabeling rule (replace) in aremoteWriteentry’swriteRelabelConfigs, an Alertmanager endpoint’srelabelingsoralertRelabelings, or a scrape class’srelabelingsormetricRelabelings(scrapeInterval: "" cannot be carried by the Prometheus operator API types (…)); the tag rule and the request-over-limit check hold with or without a policy; no policy default is filled; an image the operator chooses is refused under a registry allowlist, and the operator’s own replica and shard counts are held, not written (both below); the labels and annotations ofstorage.volumeClaimTemplateare held to the reserved metadata keys where it is the arm in use, and no claim template takes a component label. An emptyretentionis not refused: its 24h default is the operator’s code (whereretentionSizeandretentionPercentageare empty too), not a CRD default, so the derived list cannot hold it, and the object then gets the operator’s default. The operator’s behaviour is read from its source at prometheus-operator v0.94.1 (pkg/prometheus/server/statefulset.go,pkg/prometheus/common.go). The operator fills no memory request of its own for the prometheus container or the sidecar, so the memory-limit refusal ofalertmanagerhas no counterpart.No field is renamed. The spec has no top-level
labelsorannotationsfield, so every property keeps its upstream name:externalLabelsis the spec’s own field, the Prometheus external labels, not Kubernetes labels, and the reserved-key and component-label checks oflabelsdo not apply to it.Authored. The properties are the top-level json fields of
PrometheusSpec, decoded strictly at every depth, less three: the image (image,version,imagePullPolicy,imagePullSecrets); the pods, as onalertmanager; the StatefulSets (replicas,shards,shardingStrategy,serviceName,podManagementPolicy,updateStrategy,minReadySeconds,persistentVolumeClaimRetentionPolicy,shardRetentionPolicy); what it scrapes (the four selector and namespace selector pairs, the scrape settings and limits,scrapeClasses), the rules it evaluates and where alerts go (ruleSelector,ruleNamespaceSelector,rules,alerting), where its data goes (storage,retention,retentionSize,retentionPercentage,tsdb,remoteWrite,remoteRead), the Thanos sidecar (thanos), its server (web,listenLocal,enableAdminAPI), and the rest of the spec’s fields.baseImage,tagandshaare refused when not empty; an empty one writes nothing, as onalertmanager. The same three ofthanosare refused whenever set, the empty string included, since the type writes a set pointer; anullone sets none (tag: not authorable: the Prometheus operator deprecates the field, …; use image).additionalArgsandthanos.additionalArgsare passed as written and are not read. An entry ofcontainersnamedprometheusorconfig-reloader, orthanos-sidecarwherethanosis set, is a patch of the container the operator generates under that name (makeStatefulSetSpec, merged byk8s.MergePatchContainers); ofinitContainers,init-config-reloader. A patch may name no image; any other entry that names none is refused (containers[0] "proxy": names no image, and the Prometheus operator generates no container of that name to merge it into; name an image, or the container it patches (prometheus, config-reloader)), and so is athanos-sidecarentry without an image wherethanosis unset, since no sidecar is generated then. As onalertmanager, a name listed twice in one list is refused, and so is a name shared by an init container and a container of the pods, generated or listed (containers[0] "init-config-reloader": the name is also that of the init container the Prometheus operator generates, …); anddnsPolicy: NonewithoutdnsConfig.nameservers, and a pod-levelsecurityContext.windowsOptions.hostProcess: truewithouthostNetwork: true, are refused: the API refuses the pods the operator copies them into. A patch’s ports are merged into those the operator gives the container by number, as onalertmanager: theprometheuscontainer’s web port underportNameat 9090/TCP andconfig-reloader’sreloader-webat 8080/TCP, none of either underlistenLocal(config-reloaderkeeps its port under theProcessSignalreloadStrategy, to which the operator does not handlistenLocalon),init-config-reloader’sreloader-initat 8081/TCP, andthanos-sidecar’shttpat 10902 andgrpcat 10901, whichthanos.listenLocaldoes not take away. A container whose ports, as the pods run them, name two ports alike is refused (containers[0] "prometheus": ports[0] "web" at 8080/TCP: the Prometheus operator merges the patch's ports into the container's by number, which leaves another port of that name, the Prometheus operator's port at 9090/TCP, …), and so are twovolumesof one name and aserviceNamethat is not a DNS-1035 label. A nonemptyexternalUrlthat Prometheus exits on at startup is refused too, though the operator and the API accept it: the operator passes it unchanged as--web.external-url(pkg/prometheus/promcfg.go), and Prometheus exits where it begins or ends with a quote (externalUrl: begins or ends with a quote: …) or where Go’snet/urlcannot parse it (externalUrl: not a URL Go's net/url can parse: …;computeExternalURL,cmd/prometheus/main.goof v3.14.0). Prometheus checks no scheme, so any scheme builds. Neither message names the value.Required. No top-level field is one. Of what is authored below them: the
nameof an additional argument (the spec’s and the sidecar’s), a DNS option’sname, a host alias’sipandhostnames, thetypeof anupdateStrategy; thenamespaceandresourceof anexcludedFromEnforcemententry, theruleNamespaceandruleNameof aprometheusRulesExcludedFromEnforceentry;alerting.alertmanagers, and an endpoint’snameandport;apiserverConfig.host; a scrape class’sname;shardRetentionPolicy.retain.retentionPeriod;tracingConfig.endpoint; theurlof aremoteWriteand aremoteReadentry and theclientId,clientSecretandtokenUrlof itsoauth2; theclientId,tenantId(and, underazureAd.oauth,clientSecret) of a remote write entry’s Azure AD settings; and thekeyandoperatorof a match expression, in every label selector the spec holds, presence only, as on alertmanager.TestMonitoringKinds_RequiredMatchMarkersholds the list to the markers of the linked module’s source.The group of an excluded object is written. An
excludedFromEnforcemententry that leavesgroupout (or writes itnull) is written withmonitoring.coreos.com, and an authored empty group is refused by the entry’s index: the Go type writes the field even when empty, and the API admits only that one group, which it defaults where the field is absent.TestMonitoringExcludedGroup_MatchesMarkersholds the written value to the module’s default and enum.What the operator builds from the spec is checked where the API or the operator would break it, as on
alertmanagerand by the same shared checks, under any policy and none, atpkg/prometheus/common.goandpkg/prometheus/server/of prometheus-operator v0.94.1:- a
portNamethe API refuses where the operator writes it: not an IANA service name; and unlessserviceNamenames a Service of the author’s,grpcwherethanosis set, the port the operator adds for the sidecar to the governing Service it creates (MakeContainerPorts,BuildStatefulSetService; server/operator.go:1006-1030). - a negative
replicas, which the operator runs as 1 (ReplicasNumberPtr), and ashardsbelow 1, for which it runs 1 shard (shardsNumber). - the storage arms as on
alertmanager: a claim template named otherwise thanprometheus-<name>-dbbesidestorage.emptyDirorstorage.ephemeral, a claim without a positive storage request, an ephemeral claim without access modes, and on the claim template arm a claim template name that is not a DNS-1123 label or names a volume the operator adds (VolumeClaimName, server/statefulset.go:105-138). - an entry of
volumesnamed as a volume the operator adds:config,tls-assets,config-outandweb-config(the last whateverversionnames, though the operator adds it only for Prometheus 2.24.0 on);log-filewherescrapeFailureLogFileis a file name without a directory, orqueryLogFileis one andscrapeFailureLogFileis unset;thanos-prometheus-http-client-filewherethanosis set, whateverthanos.versionnames; anyprometheus-<name>-rulefiles-<n>, the volumes of the rule ConfigMaps, of which the operator mounts at least three whatever the rules (<n>a decimal without a leading zero, as the operator writes it:rulefiles-01is not reserved); the name the operator derives for each ofsecretsandconfigMaps(secret-<name>,configmap-<name>); and the data volume’s. Two entries ofsecrets, or ofconfigMaps, that the operator’s naming gives one volume name are refused as well. So is an entry whose volume name, cut to 63 characters, ends in-: the operator checks the name after the cut and fails to build the pods (ResourceNamer.DNS1123Label, common.go:289-292 and :311-314, returned at server/statefulset.go:181-184). The volumes of the web TLS credentials are not checked, as onalertmanager: the operator names each after the credential’s source with a hash appended, which the kind does not derive, so an entry under one of those names is left to the API to refuse. - an entry of
volumeMountsat a path the operator mounts a volume at in the prometheus container:/prometheus,/etc/prometheus/config_out,/etc/prometheus/certs,/etc/prometheus/web_config/web-config.yaml,/var/log/prometheuswherelog-fileis added,/etc/prometheus/rules/prometheus-<name>-rulefiles-<n>, and/etc/prometheus/secrets/<name>and/etc/prometheus/configmaps/<name>for each entry ofsecretsandconfigMaps. The mounts of the web TLS credentials, under/etc/prometheus/web_config/, are left to the API, as their volumes are. - an entry of
thanos.volumeMountsat a path the operator mounts a volume at in the thanos-sidecar container:/prometheuswhereobjectStorageConfigorobjectStorageConfigFileis set, and/etc/thanos/config, which it mounts from Thanos 0.24.0, reserved whateverthanos.versionnames, asweb-configis (createThanosContainer, server/statefulset.go:642-671 and :730-738); and any field of an entry butnameandmountPath(readOnly,recursiveReadOnly,subPath,subPathExpr,mountPropagation,bindMountOptions), which the operator drops: it copies only those two into the sidecar’s mount (:642-647).TestThanosDroppedMountFieldsholds the list to every field of the type. - a
queryLogFilenamed without a directory beside ascrapeFailureLogFilenamed with one: the operator writes the query log under/var/log/prometheusbut adds thelog-filevolume there only for the scrape failure log file, on a read-only root filesystem (server/statefulset.go:510, :537; common.go:334-347). A writable volume the author mounts there, involumeMountsor a patch of theprometheuscontainer, is accepted; a mount there withreadOnlyset, in either, is not. The path is compared cleaned (/var/log/prometheus/is the same directory). Where both name the path alike, the operator’s strategic merge keepsreadOnlyif either sets it; where they spell it differently, both mounts stay, and which one the container runtime mounts over the other is not derived, so the read-only one still refuses. - a negative request or limit in
resources,thanos.resourcesor a listed container. - a name the operator’s objects cannot be named after: the data volume
prometheus-<name>-db, the volume of the third rule ConfigMapprometheus-<name>-rulefiles-2, and the hostname of the last pod of the last shard,prometheus-<name>-<replicas-1>orprometheus-<name>-shard-<shards-1>-<replicas-1>, must each be a DNS-1123 label; and the name of the last shard’s StatefulSet,prometheus-<name>orprometheus-<name>-shard-<shards-1>, must be at most 52 characters: the StatefulSet controller labels each of its podscontroller-revision-hashwith that name, a-and a hash of up to 10 characters, a label value the API refuses beyond 63 characters. So a name has no dot and at most 40 characters, 33 with 2 to 10 shards, and fewer with more. Neither applies wherereplicasis 0, with no pod to name or label. The operator mounts three rule ConfigMaps whatever the rules (server/rules.go:119), and more only where the PrometheusRules it selects outgrow three of half a MiB each; the name of an eleventh,rulefiles-10, depends on the cluster’s rules and is not checked. The refusal names the component, orobjectNamewhere that set the name.
The API’s expression rules are not checked. Five are listed (
prometheusRulesLeft) and left to the API server: with the sharding strategy modeTopology,shardsmust be at least the number of topology values, andtopologyneeds that mode; anupdateStrategywith arollingUpdatemust have the typeRollingUpdate; and theexternalIdof asigv4, a remote write entry’s or an Alertmanager endpoint’s, needs aroleArn.What the type writes unauthored:
arbitraryFSAccessThroughSMs: {},resources: {}andrules: {alert: {}}, on every Prometheus; on an authoredthanos, itsresources: {}; and, on an authoredstorage, avolumeClaimTemplatewithmetadata: {}andstatus: {}.Policy. Refused, with a workload kind’s classes:
image,thanos.image, the image of a listed container or init container (a patch included) and an image volume’s reference outside the allowed registries; under a policy that lists allowed registries, an image the operator chooses (below);replicastimesshardsover the replica maximum, as the operator counts them (below;replicas times shards 4 exceeds enforced maximum 3); the cpu or memory ofresources, ofthanos.resourcesand of a listed container over the maxima; the storage a claim requests over the storage maximum; a hostPath volume;hostNetworkwhere the policy does not allow the host network; a privileged listed container, a pod-levelsecurityContext.windowsOptions.hostProcessand a capability the policy does not allow.TestMonitoringWorkloadKinds_PodFieldsHeldOrListedholds every top-level field of the spec that shapes the pods to a refusal shown on a document or a stated reason;shardsand the sidecar’s fields are not top-level pod fields of the derivation, andshards,thanos.imageandthanos.resourcesare held byTestPrometheus_ReplicasTimesShardsandTestPrometheus_PolicyRefusalsinstead.An image the operator chooses is refused under a registry allowlist. Where the spec names no image for a container the operator generates, the operator chooses the one it runs, and no registry allowlist reaches that choice, so a policy with a non-empty
AllowedRegistriesrefuses it (image: unset, so the Prometheus operator chooses the image the pods run, …;thanos.image: unset, …;the image of the config-reloader container (containers): unset, …) and the author names an image from one of the listed registries:imageor a patch ofprometheus; wherethanosis set,thanos.imageor a patch ofthanos-sidecar; a patch ofconfig-reloader; and a patch ofinit-config-reloaderininitContainers. The operator generates the prometheus container, both reloaders and, wherethanosis set, the sidecar (makeStatefulSetSpec,createThanosContainer,BuildConfigReloader). Without such a list, it builds.The operator’s own counts are held, not written. The operator runs one StatefulSet a shard with the replica count each, and reads an unset or negative
replicasas 1 and an unsetshardsas 1 (ReplicasNumberPtr,shardsNumber; ashardsbelow 1 is refused). The replica maximum holds that product whether or not either is authored, and the emitted object still leaves both as authored.Credentials: three are held, the rest are stated. The deprecated
bearerTokenof aremoteWriteentry, of aremoteReadentry and ofapiserverConfigholds a credential in the object; under a policy that forbids explicit secrets each is refused (oam.RefusalExplicitSecret), and without one it is written as authored. Every other credential of the spec is the key of a Secret or the path of a file in the container. Not held: a value of aheadersmap (remoteWrite,remoteRead,tracingConfig), the userinfo of a URL, and free text (additionalArgs, theenvof a listed container) are written as authored, under a policy that forbids explicit secrets too.TestMonitoringWorkloadKinds_CredentialsHeldOrListedderives the fields whose name suggests a credential and holds each to the policy or a stated reason.podMetadata. As onalertmanager: the pods carry the component label only if the author writes the component’s own value intopodMetadata.labels. The operator reserves seven labels of its own on the pods (prometheus,app.kubernetes.io/instance,app.kubernetes.io/managed-by,app.kubernetes.io/name,app.kubernetes.io/version,operator.prometheus.io/name,operator.prometheus.io/shard) and the annotationkubectl.kubernetes.io/default-container. The operator copies the authored labels and annotations overapp.kubernetes.io/versionand the annotation, so an authored value replaces those two, and then sets the other six labels over the authored ones, so an authored value of those does not hold (BuildPodMetadata, promcfg.go:1367-1384; server/statefulset.go:219-230). Launcher does not refuse such a key.Not covered. Whether what the object refers to exists (a Secret key, a mounted Secret or ConfigMap, the ServiceAccount and the RBAC Kubernetes service discovery needs, a governing Service named by
serviceName, the objects the selectors match, the Alertmanager endpoints). What the operator adds on its own beyond the containers above (theprometheus-operatedService, the configuration Secret) is not in the object; its code is not in the linked module, and was read at the version above for the containers, their images and the pod count only. The object’s status is the operator’s and is not written.- a
webservice / worker —
image,replicas(default 1),port(webservice), plus the fullDeploymentSpec-level surface they share withdeployment—strategy,minReadySeconds,revisionHistoryLimit,pausedandprogressDeadlineSeconds(go-kure/launcher#341, see “Deployment-level properties” above). What still separates them fromdeploymentis launcher’s own opinions, which these two keep:topologySpreadand the four-keyaffinityshorthand, and onwebservicetheportthat drives the Service. Note the shorthand is what they keep — neither publishes the rawcorev1affinity/topologySpreadConstraintsthatdeploymentdoes. Thewebservicerule implements the optionaloam.EndpointProvider: it declares its own pods (app: <component-name>) on the declaredport(its singleportproperty drives both the container port and the Service port), letting a downstream platform synthesize generic app→app connections targeting a webservice.workerdeclares no in-cluster port and emits no Service, so it deliberately advertises no endpoint (not anEndpointProvider).workeris a component lowering rule, not a handler (WorkerRule, go-kure/launcher#280). It runs worker’s own parse, then re-expresses the component as adeploymentof the same name: the authored properties are forwarded, the four-keyaffinityshorthand is evaluated exactly as before and forwarded as the rawcorev1affinity(omitted when it evaluates to nothing), andtopologySpreadbecomes a synthesizedtopology-spreadtrait placed before the authored traits (none whentopologySpread: false). Authored traits and annotations are forwarded unchanged. Worker’s published schema, its generated output and theapp: <component-name>selector are unchanged — every example and golden fixture builds byte-identically — and the emitted component carriesOrigin.Rulecomponent/worker@v1alpha1. Two differences are deliberate. A worker refused by its own parse now readscomponent "w" (type "worker") in document …: <cause>(the lowering engine’s prefix) where it readcomponent "w": <cause>; the cause is unchanged. A trait-lowering error on a worker’s trait gains one chain line naming thecomponent/worker@v1alpha1step; a trait handler’s error reads as before. The former handler’s affinity label-syntax check (see Common config) runs in the rule, before the rawaffinityis forwarded, with the sameaffinity: the shorthand evaluates to an affinity the API server would refuse: …text. A key worker’s parse refuses with a reason is refused there: an upstream field of its refusal maps, orpodActiveDeadlineSeconds, which only Job pods may set. Any other key worker does not declare is dropped rather than forwarded todeployment;kurel buildrefuses both before lowering anyway.- Both rules emit the component’s ServiceAccount as a
serviceaccountmember (go-kure/launcher#702) unlessserviceAccountNameis authored. Aworkertherefore lowers to a same-name sibling group of adeploymentand aserviceaccount; awebserviceto adeployment, aserviceand aserviceaccount. The member carriesautomountServiceAccountToken: false, the component’s annotations, and the authoredprune-protectionandforce-replacetraits. Thedeploymentmember is handed the account’s object name asserviceAccountName(the component name, unlessserviceAccountObjectNameor theNaminghook names the account, below), so it runs as that account and generates none of its own. The output is unchanged: the same ServiceAccount, in the same place, so every golden builds byte-identically. One consequence for a library caller: a registry that lowerswebserviceorworkermust register aserviceaccountcomponent handler too, or the build fails withmember type "serviceaccount" has no component handler. - Both rules turn each claim a
pvcvolume describes into a synthesizedpvctrait on thedeploymentmember (go-kure/launcher#702), named<component>-<volume>as before unless the volume’sclaimObjectNameor theNaminghook names it (below), and rewrite the volume to reference it byclaimName, keepingaccessModesso the non-RWX constraints still see it. The claims are generated as before, after the component’s own objects. A registry that lowerswebserviceorworkermust register thepvctrait handler too. webserviceis a component lowering rule, not a handler (WebserviceRule, go-kure/launcher#280). It runs webservice’s own parse, then re-expresses the component as a same-name sibling group (seepkg/oam“Same-name sibling groups”): adeploymentand then aservice, both named after the component, deployed as one component — one tier, one bundle. Thedeploymentmember gets the authored properties webservice declares exceptport,topologySpreadandaffinity, plus the main container’s one port{name: http, containerPort: <port>}; theaffinityshorthand andtopologySpreadare handled as onworker. Theservicemember gets one port{name: http, port: <port>}and the default selectorapp: <component-name>. Annotations go to both members. Each authored trait is forwarded unchanged, in authored order, to the member it acts on:expose,ingressandhttproutego to theservice;prune-protectionandforce-replacego to both; every other trait goes to thedeployment. That includesfluxcd-patchesandfluxcd-postbuild(not built in; a consumer that delivers through Flux registers them) and any trait type an extension registered, since thedeploymentmember holds the pods. Webservice’s published schema and its generated objects are unchanged, in the handler’s order (Deployment, Service, ServiceAccount, claims). Each emitted component carriesOrigin.Rulecomponent/webservice@v1alpha1. Four differences are deliberate. A refusal from webservice’s own parse gains the lowering engine’s prefix, as onworker; the cause is unchanged. The synthesized inbound NetworkPolicy opens an ingressportName: httpas the port’s number, where the handler opened the namehttp, which the container port also carried; the pods admitted are the same. Likewise, an ingress path on another component whosebackendnames a webservice’s Service byportNameopens that port’s number on the webservice’s pods, translated as on aservicecomponent. A route on another component whosebackendnames a webservice’s Service on a port that Service does not expose (port: 9090on a webservice whoseportis 8080) no longer opens that port on the webservice’s pods: theservicemember opens only its own declared ports, as aservicecomponent does. The route was already broken, since the Service has no such port to forward. Webservice is no longer a handler a library caller converts with directly, so the nameless-config fallback the handlers keep does not apply to it: the engine only lowers a named component.- A
webservicecomponent name must be a valid Service name. It always emits a Service named after the component, and the API server validates a Service’smetadata.nameas a DNS-1035 label: at most 63 characters, lowercase letters, digits and-, starting with a letter and ending with a letter or digit. That is stricter than the DNS-1123 subdomain every component name already passes, soapi.v1, a 64-character name and1apiare refused at conversion (name: "1api" is not a valid Service name, which must be a DNS-1035 label) instead of building a manifest the cluster rejects on apply (go-kure/launcher#546).Generateapplies the same rule to the Application name it is handed.workeremits no Service and keeps accepting such a name, within the container-name rule above. The rule holds for the component name whatever the Service is named:serviceObjectName(below) does not lift it. deploymentObjectName,serviceObjectNameandserviceAccountObjectNamename the objects the component generates (go-kure/launcher#787), each in place of the component name and each on its own: the Deployment, the Service (webserviceonly;workergenerates none and does not declare the property) and the ServiceAccount. A name is used as written, never shortened. The Deployment’s and the ServiceAccount’s are DNS-1123 subdomains, the Service’s a DNS-1035 label; anything else is refused (naming the Service: serviceObjectName "web.v1" cannot be the name for role "workload-service": not a valid DNS-1035 label: …). Without the property theNaminghook is asked (rolesworkload-deployment,workload-service,workload-serviceaccount), and without an answer the object keeps the component name, so a document that sets none builds byte-identically. Each name is held against every other resolved name of its kind and namespace: two components given oneserviceObjectName, or aservicecomponent whose object carries awebservice’s Service name, are refused as a name collision with both named.- They name the objects alone. The members keep the component’s name, so
the
applabel, the pod template’s labels, the Deployment’s and the Service’s selectors, the main container, the claims’ default names (<component>-<volume>) and every name a trait derives (<component>-hpa,<component>-httproute) stay as they were. - A
pvcvolume’sclaimObjectNamenames the claim that volume generates (roleworkload-volume-claim), in place of<component>-<volume>, used as written: a DNS-1123 subdomain, never shortened. The volume mounts the claim by that name. Without it theNaminghook is asked once per such volume. A volume withclaimNamereferences an existing claim and generates none, soclaimObjectNamebeside it is refused and the hook is not asked for it. Two volumes, or a volume and another component, that give one claim name are refused as a name collision. - What launcher writes to a renamed object follows it. The
scalertrait’sscaleTargetRefnames the Deployment, whatever the Service and the ServiceAccount are named. A routing trait’s own backend (expose,ingress,httproute) is the Service, and a route naming that Service still resolves to the component for the synthesized NetworkPolicy. The pods’serviceAccountNameand therbactrait’s subject name the ServiceAccount. - A renamed Service changes its DNS name: the Service’s name is the name
it is reached at in the cluster. Launcher builds no such address, so every
address written with the component name (an
envvalue, a URL in another component’s properties, a backend another component’s route names) is the author’s to change. serviceAccountObjectNamenames the account the component generates,serviceAccountNamean account that exists. WithserviceAccountNamethe component generates none, so the two together are refused (serviceAccountObjectName and serviceAccountName are both set: …), and the hook is not asked for an account.
- They name the objects alone. The members keep the component’s name, so
the
- An ingress
portNameon awebservicemust behttp. The Service’s one port is namedhttp, so aningresspath whose implicit backend is this component’s Service (nobackend, orbackendnaming that Service) and that addresses it by any otherportNameis refused at build time (cannot route implicit backend to port "name"), just as a numberedportother than the component’sportis (go-kure/launcher#545).
deployment — the kind-named Deployment (see “Deployment-level properties” above): the shared container-level, pod-level and
DeploymentSpec-level surface, the last of which it now shares withwebserviceandworkerrather than owning. What distinguishes it is mostly what it leaves out: notopologySpread, no four-keyaffinityshorthand, noport; it declares no endpoint and emits no Service. It is notworkerminus a few things either — it reads nulls as omissions across its whole surface, which the role kinds do not. The one thing it adds is the rawcorev1scheduling surface the role kinds lack:affinity,tolerationsandtopologySpreadConstraintsas the API shapes (go-kure/launcher#412, see “Raw scheduling properties” above). That is the same distinction in the other direction — the role kinds carry the opinion, this kind carries the API.service — the kind-named Service (go-kure/launcher#411), an independent component rather than half of a workload: it emits the Service and nothing else, and the workload component whose pods it selects owns their ServiceAccount.
selectordefaults toapp: <component name>, the label every workload kind puts on its pods; set it when the Service fronts a workload named differently (adeploymentnamedapi-serverbehind aservicenamedapi).portsis the fullcorev1.ServicePortlist, at least one entry unless the Service is headless or of typeExternalName:port(required),targetPort(a number or a container port name, published as theTypes: [integer, string]union (go-kure/launcher#383), so a value of any other type is rejected by property validation before the parser sees it; defaults toport),protocol(TCP,UDPorSCTP; defaults toTCP),name(required once there is more than one port; names and port/protocol pairs must be unique),nodePort(1-65535, only withtypeNodePortorLoadBalancer, unique per protocol, as the API requires; two refusals are launcher’s own:0, which the API reads as “allocate one” — leave the key out instead — and a node port on anExternalNameService, which the API accepts; the cluster’s own node port range is narrower and is checked on apply) andappProtocol(a qualified name such ashttporkubernetes.io/h2c).typeisClusterIP(default),NodePort,LoadBalancerorExternalName. An emptyselectoris refused.ExternalName.
type: ExternalName(go-kure/launcher#790) emits a DNS alias forexternalName, which is then required: a DNS-1123 subdomain, with one trailing dot allowed. The Service selects no pods, soselectoris refused with it (launcher’s own rule; the API accepts and ignores one), and so areclusterIP: None,ipFamiliesandipFamilyPolicy;portsis optional. On any other typeexternalNameis refused (launcher’s own rule; the API ignores it there). Whatever ports it lists, an ExternalName Service has no first port for a routing trait and no NetworkPolicy endpoint: aningressorhttproutetrait refuses it as an implicit backend (component "db" has no service port), a trait-levelservicePortis refused, and it declares no endpoint. As on a port-less headless Service, the refusal is of the component as a backend, not of the trait: a trait on it that names another Service explicitly is carried as on any component, and that route’s policy is the routed Service’s. It still owns its name (BackendServiceName): a route from another component naming it is a route to a Service of this application, so that route’sbackendSelectoris not trusted, and no inbound NetworkPolicy is synthesized for that route, in a same-name sibling group either: the Service leads to no pods of the cluster, and the group’s component label is on its workload’s pods, which the Service does not lead to. Named apart from its component (objectName), it owns that name instead: a route naming the Service’s name gets no policy, and one naming the component’s name is a route to a Service from elsewhere.Further
ServiceSpecfields (go-kure/launcher#790). Each is written only when authored; unauthored, the API server’s own default applies. The rules are the API server’s (validateService), except where marked as launcher’s own.Property Rules externalTrafficPolicyClusterorLocal. Only withtypeNodePortorLoadBalancer.internalTrafficPolicyClusterorLocal.trafficDistributionPreferSameZone,PreferSameNode, orPreferClose(the deprecated name ofPreferSameZone).sessionAffinityNoneorClientIP.sessionAffinityConfigOnly with sessionAffinity: ClientIP.clientIP.timeoutSecondsis required once the key is authored (launcher’s own: the API server would default it), 1-86400.publishNotReadyAddressesA boolean; written only when true.ipFamiliesIPv4and/orIPv6, each once, at most two.ipFamilyPolicySingleStack,PreferDualStackorRequireDualStack;SingleStackis refused with twoipFamilies(the API server’s allocator refuses it).loadBalancerClassA qualified name. Only with type: LoadBalancer.loadBalancerSourceRangesCIDRs in canonical form (launcher’s own: the API tolerates surrounding spaces and some non-canonical forms). Only with type: LoadBalancer.loadBalancerIPOnly with type: LoadBalancer(launcher’s own: the API ignores it elsewhere). The value is not validated, as in the API.allocateLoadBalancerNodePortsA boolean. Only with type: LoadBalancer.healthCheckNodePort1-65535. Only with type: LoadBalancerandexternalTrafficPolicy: Local.0is refused (launcher’s own: the API reads it as “allocate one”; leave the key out instead).externalIPsandclusterIPsare refused, and so is a literalclusterIPaddress.externalIPsroutes traffic for addresses the cluster does not manage and is deprecated by the API; a Service’s cluster IPs are the cluster’s to allocate. In a document all three are refused by property validation before the parser runs:externalIPsandclusterIPsas unsupported fields, each with the kind’s reason after the list of allowed keys (go-kure/launcher#790), and a literalclusterIPaddress as a value outside the property’s enum, which hasNonealone.Headless.
clusterIP: None(go-kure/launcher#690) emitsspec.clusterIP: None: no virtual IP, and cluster DNS resolves the name to the selected pods, as a StatefulSet’s governing Service needs. It requirestype: ClusterIP(the default), since the API server refuses a headlessNodePortorLoadBalancerService.Noneis the only value: a literal address is refused, and so is an empty string, rather than read as absence. A headless Service may have noportsat all; withoutclusterIP, at least one port is still required (ports: at least one port is required), except on anExternalNameService (above). A port-less Service has no first port, so routing traits refuse it as an implicit backend, refuse a trait-levelservicePorton it, and it declares no NetworkPolicy endpoint. It still owns its name (BackendServiceName): a route from another component naming it is a route to a Service of this application, not an external backend, so that route’sbackendSelectoris not trusted and, with no TCP port to translate, no allow is synthesized. WithoutclusterIPthe Service carries noclusterIP, as before.The component name must be a valid Service name. It becomes the Service’s
metadata.name, which the API server validates as a DNS-1035 label: at most 63 characters, lowercase letters, digits and-, starting with a letter and ending with a letter or digit. That is stricter than the DNS-1123 subdomain every component name already passes, soapi.v1, a 64-character name and1apiare refused here (name: "api.v1" is not a valid Service name, which must be a DNS-1035 label) instead of building a manifest the cluster rejects on apply.Generateapplies the same rule to the Application name it is handed.Routing traits use the first port.
ingress,httprouteandexpose(which lowers to one of the two) on aservicecomponent resolve their implicit backend toports[0].portand refuse any other port on it, whether it is named by number (cannot route implicit backend to port N) or by an ingress path’sportName(cannot route implicit backend to port "name"); aportNamemust be the first port’s own name, so an unknown name is refused too. Naming the component’s own Service as the backend (backend: apion an ingress path,backendRefs: [{name: api, port: 9000}]on an httproute) is still the implicit backend and is refused the same way. To reach a later port, put the routing trait on another component and name this Service there as an explicit backend, with that port.Synthesized NetworkPolicy. The
{component}-allow-ingress-trafficpolicy for traffic routed to aserviceselects itsselectorpods — not the component label, which no pod carries — and opens thetargetPortof each routed TCP port. A route that reaches only a UDP or SCTP port synthesizes no policy, and neither does a route to anExternalNameService, which has no pods to open a port on (above). In a same-name sibling group whoseselectorpicks a sibling’s own pods, the policy selects the component label instead, but only whenIdentityTargetPortsis true: the Service has at least one port and every port, whatever its protocol, has a numerictargetPortequal to its ownport. Otherwise it keeps theselector. Either way the routed ports are translated to theirtargetPortand only TCP ones are opened (seepkg/oam“Same-name sibling groups”).It implements
oam.EndpointProvider: one endpoint, theselectorpods on every TCPtargetPort(deduplicated). A Service with no TCP port declares none, and neither does anExternalNameService.No policy check.
servicehas noApplyPolicy, deliberately (go-kure/launcher#794, item 2): none of its properties maps to an environment-policy method, so there is nothing to enforce. The policy makes no statement about a Service’stype, soNodePortandLoadBalancerbuild under every policy.type: ExternalNamehas no capability gate either: whoever may author aservicecomponent may give an in-cluster name to any DNS name outside the cluster, and workloads that resolve the Service name are sent there.nodePort,loadBalancerSourceRanges,loadBalancerIPandexternalTrafficPolicyare accepted the same way. The object kind policy (oam.ObjectKindPolicy, go-kure/launcher#922) holds kinds, not these fields: it can keep Services out of a build, not onetype.
serviceaccount, persistentvolumeclaim, configmap (go-kure/launcher#702) are kind-named projections of one object each. Each emits that object, named after the component, and nothing else, so another component refers to it by the component name. They are the authorable forms of the objects the workload kinds generate for themselves today, and they carry no launcher opinions. Like every component, they are in no tier unless a tier annotation or placement policy places them.
serviceaccountandconfigmaphave noApplyPolicy, deliberately (go-kure/launcher#794, item 2): none of their properties maps to an environment-policy method (imagePullSecretsnames Secrets, not a registry), so there is nothing to enforce.persistentvolumeclaimhas one, for the storage default and maximum.serviceaccountpublishesautomountServiceAccountTokenandimagePullSecrets([{name}]). An unauthoredautomountServiceAccountTokenstays unset, which Kubernetes reads as true. The account a role kind generates sets it to false, so aserviceaccountreplacing that account authorsfalseto keep the same posture. A workload runs as it throughserviceAccountName, which also stops a role kind from generating its own account.persistentvolumeclaimpublishessize,storageClassName,accessModes(default[ReadWriteOnce]) andvolumeMode. The claim is built by the sameBuildPVCas apvcvolume, so the two agree on every field they share, including the explicit-emptystorageClassName: "", which requests no class. Thepvctrait is this kind’s twin (go-kure/launcher#741): it runs the kind’s ownParseClaimProperties,ApplyClaimPolicyandGenerateClaim, so the same properties build the same claim. Only the ownership fields differ: the claim’s name, itsapplabel, namespace and bundle. An unauthoredstorageClassName(absent ornull) comes from the ClusterProfilepvccapability’sstorageClassName, as the trait’s does (go-kure/launcher#742); with no binding it stays unset, which is the cluster’s default class. Pre-GA output change: the kind used to ignore that binding, so an existing claim built from it got the cluster’s default class when it was created, if the cluster had one. A claim’s assignedstorageClassNamecannot change: if the binding’s class differs from it, the claim fails to apply until it is recreated or the assigned class is authored. A claim created with no class may take one later, so it applies. An unauthoredsizecomes from the EnvironmentPolicy storage default; with neither, the build fails withsize: required …. The policy’s maximum storage size applies either way. A workload mounts the claim through apvcvolume’sclaimName(see “Referencing an existing claim” below). A claim name another component also generates (for example apvctrait of the same name) is refused as a generated-object collision.Four more claim-spec fields are read (go-kure/launcher#790), by the kind and by the
pvctrait alike (claim_spec.go). Each is written only when authored, so a document that authors none builds the claim it built before. A workload’spvcvolume does not take them.Property Rule selector{matchLabels, matchExpressions[]}A label query over the PersistentVolumes that may back the claim, parsed as a statefulsetclaim template’s is. A claim with a selector is never dynamically provisioned. An empty selector is refused: this is launcher’s own rule, the API server reads it as “every volume”.dataSourceRef{apiGroup, kind, name, namespace}The object to populate the volume from, with the rules of upstream validateDataSourceRef(thedataSourceRefrow under “StatefulSet-level and claim-template properties”).volumeNameThe name of the PersistentVolume to bind to. Must be a DNS-1123 subdomain: launcher’s own rule, since ValidatePersistentVolumeClaimSpecdoes not read the field and a value that cannot name a PersistentVolume would leave the claim Pending for good.volumeAttributesClassNameA DNS-1123 subdomain ( ValidateClassName).No policy check on
volumeName. It is accepted with no capability gate and no EnvironmentPolicy method; the object kind policy (oam.ObjectKindPolicy, go-kure/launcher#922) holds kinds, not this field. A claim that names a volume binds to that PersistentVolume and to no other, with no dynamic provisioning, so whoever may author a claim may ask for any PersistentVolume of the cluster by name. The cluster still decides: the volume must be unbound or reserved for this claim, and must satisfy the claim’s size, access modes, class, attributes class and volume mode (the PersistentVolume controller’scheckVolumeSatisfyClaim). The storage default and maximum still apply to the claim’ssize.dataSourceis refused: it is the superseded spelling ofdataSourceRef, which the API server mirrors into it. The longresourcesspelling ofsizeis not read on a standalone claim.configmappublishesdata(string values only: a number or a boolean is refused rather than stringified),binaryData(base64; a key may not also appear indata) andimmutable. Thedatavalues and the decodedbinaryDatavalues together may hold at most 1,048,576 bytes, the limit the API server enforces; a larger ConfigMap is refused at build time. That check is the exportedCheckConfigMapSize(data, binaryData). A key the API server refuses in a ConfigMap is refused by the exportedValidateConfigMapKey(field, key). Keys are checked in sorted order, so with several bad entries the one reported is the same on every build. It is a different type from theconfigmaptrait, which attaches a ConfigMap to another component. The trait is this kind’s twin (go-kure/launcher#741): it readsdata,binaryDataandimmutablethrough the exportedParseConfigMapProperties(props)and builds the ConfigMap throughGenerateConfigMap(config, name, namespace, labels), so the same properties give the same ConfigMap and the same refusals on both paths.
secret (go-kure/launcher#790) is the kind-named projection of a v1 Secret, on the recipe of
configmap: it emits the Secret, named after the component (or itsobjectName) and carrying the component’sapplabel, and nothing else, so a workload’ssecretvolume,envFromorsecretKeyRefnames it by that name.- It publishes
stringData(plain string values: a number, a boolean or a nested object is refused, not stringified),data(base64; a key may not also appear instringData, where the API server would letstringDatawin silently),typeandimmutable. An unauthoredtypeandimmutablestay unset (the API server storesOpaquefor an unset type), and the keys a type requires are left to the API server. - Every entry is emitted under
data, base64-encoded as the API serializes it, and never understringData: the object written is the object the API server stores. base64 is an encoding, not encryption, so the build output is as sensitive as the document, and so is every place that output is committed or pushed to. Prefer a Secret created out of band (anexternal-secrettrait, a sealed or externally managed Secret) for anything that must not be in the output. - The decoded values together may hold at most 1,048,576 bytes, the limit
the API server enforces (the exported
CheckSecretSize(data)); a key the API server refuses is refused by the exportedValidateSecretKey(field, key). Keys are checked in sorted order. No refusal carries a value: an entry is named by its key, a wrong value by its type, and a base64 error by its key alone. - Policy. Unlike
configmap, the kind has anApplyPolicy: a policy that forbids explicit secrets (oam.ExplicitSecretPolicy, see thepkg/oamREADME) refuses the component with a violation naming it,secret: the environment policy forbids explicit secrets; reference a Secret created out of band instead, as it refuses thesecrettrait, ahelmcomponent’ssecretValuesand a Secret apassthroughormanifestscomponent carries. A policy that does not implement that interface, and no policy, allow it. - It is a different type from the
secrettrait, which attaches a Secret to another component. The trait is this kind’s twin: it reads the same four properties through the exportedParseSecretProperties(props)and builds the Secret throughGenerateSecret(config, name, namespace, labels), and its schema is the kind’s plusname, so the same properties give the same Secret and the same refusals on both paths. Only the ownership fields differ: the Secret’s name, its labels, its namespace and the bundle it is placed in. - Names. The kind’s Secret is claimed under role
object. Asecretcomponent and asecrettrait naming one Secret are refused; the trait itself claims no name. It names its own Secret under no role, so the two meet among the generated objects (generated-object collision: Secret "shop/shared" is generated by both component "shared" and sub-application "shared" of component "web"; …), as aconfigmapcomponent and trait do. The Secret ahelmcomponent generates forsecretValuesis claimed under rolevalues-secret, as the same kind, so asecretcomponent under that name is refused as a name collision with both named.
- It publishes
namespace, limitrange, resourcequota (go-kure/launcher#790) are kind-named projections of one Kubernetes core object each, built on the recipe of the
cnpg-pooler,cnpg-databaseandcnpg-objectstorekinds: one schema key per json field of the object’s spec type, the whole property map decoded strictly into that type under the null contract, and two spellings of one field refused.TestCoreKindSchemas_CoverSpecholds each schema to the linked type by reflection. Each emits its object, named after the component, with the authored spec and nothing else: the handler adds no annotation and no label of its own, not anapplabel either. The transform then sets the component label on the object, as on every object a component owns (go-kure/launcher#788). None runs a pod or requests storage, soApplyPolicyis a no-op. Like every component, they are in no tier unless a tier annotation or placement policy places them. The properties are the spec fields, andlabelsandannotationsfor the object’s metadata (“The object’s labels and annotations” below).namespacepublishesfinalizers, the one field ofcorev1.NamespaceSpec. A Namespace is cluster-scoped: the object carries no namespace, whatever namespace the application is built for. The component name is the Namespace’s name, which the API holds to a DNS-1123 label (at most 63 characters, no dot), narrower than a component name; any other name is refused, naming it. A Namespace that needs the Pod Security Admission labels (pod-security.kubernetes.io/enforceand its siblings) writes them underlabels.limitrangepublisheslimits, the one field ofcorev1.LimitRangeSpec, and emits the LimitRange in the build namespace.limitsis required: the Go type cannot omit it, so an unauthored one (absent ornull) would be written aslimits: nulland is refused withlimits: required …. An authoredlimits: []is kept: a LimitRange that enforces nothing. Each limit’stypeis required for the same reason (limits[1].type: required …), and two limits of one type are refused by path, as the API refuses them. The other value rules (which type names exist,minat mostmax, what aPodlimit may not set) are left to the API server. A quantity written as a number is emitted in its canonical string form (cpu: 2becomescpu: "2"). The limits constrain the pods and claims of the namespace at admission; the environment policy is not applied to them.resourcequotapublisheshard,scopesandscopeSelector, the fields ofcorev1.ResourceQuotaSpec, and emits the ResourceQuota in the build namespace. The API requires none of them, so a component with no properties is a quota that limits nothing. Which resource names, scopes and selector operators exist is left to the API server. A quantity written as a number is emitted in its canonical string form (persistentvolumeclaims: 10becomes"10"). The quota bounds what the namespace may hold in total; the environment policy is not applied to it.
persistentvolume (go-kure/launcher#790) is the kind-named projection of a v1 PersistentVolume, on the recipe of
namespace,limitrangeandresourcequota: one schema key per json field ofcorev1.PersistentVolumeSpec, the property map decoded strictly into that type, two spellings of one field refused, and one object named after the component, with the authored spec; the handler adds no label and no annotation of its own, and the transform sets the component label. The spec embedscorev1.PersistentVolumeSource, so each volume source (nfs,csi,hostPath, …) is a top-level property, as it is a top-level field of the object’s spec. A PersistentVolume is cluster-scoped: the object carries no namespace, whatever namespace the application is built for. The API requirescapacity,accessModesand exactly one source; those and the other value rules are left to the API server, so a component with no properties builds a volume the server refuses. A quantity written as a number is emitted in its canonical string form. The object’s labels and annotations are thelabelsandannotationsproperties.Policy. Unlike
namespace,limitrangeandresourcequota, a PersistentVolume is held to the environment policy (enforcePersistentVolumePolicy):- A
hostPathorlocalsource is refused unlessAllowHostPathVolumes()allows it. Both name a path on the node (locala disk, partition or directory there), and a pod that binds the volume through a claim reads and writes that path, as it does through ahostPathvolume of its own. capacity.storageis held to the storage maximum (MaxStorageSize()), as the storage a claim requests is.
One check covers every path that can produce the object: this kind, and the rendered-object check that template delivery (
helmtemplate),passthroughandmanifestsrun, where the refusal names the object (passthrough: object PersistentVolume "data": spec.hostPath: …). On those three paths a PersistentVolume in an API version the build cannot read is refused, since its source and capacity cannot be checked.Of the other source types of
corev1.PersistentVolumeSource(k8s.io/api v0.37.1,core/v1/types.go), all butcsiandflexVolumename a remote endpoint or disk, not a path on the node, and are not gated. Not covered: acsiorflexVolumesource. Each names a driver and options only the driver interprets, and no field says whether a node path is exposed; a pod’scsiandflexVolumevolumes are unchecked for the same reason. A driver that exposes a node path therefore passes the gate. This is a decided limit (go-kure/launcher#794, item 12): the environment policy makes no statement about driver-defined volumes, on pods or on PersistentVolumes. One (an allowlist of driver names, for instance) would be a new part of the policy every consumer implements, and no use case asked for it.Breaking: before this kind, a PersistentVolume that a chart rendered, a
passthroughcomponent held or amanifestssource yielded reached the output unchecked. One with ahostPathorlocalsource, or withcapacity.storageover the storage maximum, is now refused where it built before — and with no policy passed ahostPathorlocalone is always refused, sinceNoopPolicyallows no hostPath volume. A consumer allows it through its policy (AllowHostPathVolumes(),MaxStorageSize()), not per component.- A
pod (go-kure/launcher#790) is the kind-named projection of a v1 Pod, on the same recipe: one schema key per json field of
corev1.PodSpec, the property map decoded strictly into that type, two spellings of one field refused, and one Pod named after the component in the build namespace, with the authored spec. The handler adds theapplabel and nothing else: no annotation, no ServiceAccount, noautomountServiceAccountToken, no resources, no probe. Theapplabel (app: <component>, see “Theapplabel”) is the one every workload kind gives its pods and the one launcher’s traits and Services select on: anetworkpolicytrait’s policy matches the Pod by it, and aservicecomponent reaches it withselector: {app: <component>}. The transform then sets the component label, as on every object a component owns (go-kure/launcher#788). It is a bare Pod: no controller recreates it, and it is not the workload shape ofwebservice,workerordeployment— a property of those (image,ports,envat the top level) is refused as not a PodSpec field.Traits. The pod is a trait target:
security-context, aconfigmaptrait’smountPathand anexternal-secrettrait’senvFrom/mountPathchange its spec as they change a workload kind’s pod template.topology-spreadis Deployment-only and refuses apodcomponent.Refused when the component is read, with or without a policy:
ephemeralContainers,priorityandoverhead, with the texts the workload kinds give. A pod cannot be created with ephemeral containers, and the Priority and RuntimeClass admission controllers set the other two and reject a differing value; an author setspriorityClassNameorruntimeClassName. The three are not in the schema, so a caller that validates the properties against it first, askurel builddoes, refuses each as an unsupported field whatever its value, an empty one included. A caller that converts the component without that validation gets the texts above, and there an emptyephemeralContainersoroverheadis read as unset. Cost: on a cluster that runs without those admission controllers the two fields cannot be authored through this kind.An unauthored
containers. The API’s other value rules (an empty list, a container without a name, …) are left to the API server.An image without a tag or digest, or tagged
:latest, on every init and regular container (ValidateImageRef), so a container without an image too.The same on the
referenceof an image volume (volumes[0] "ext" image.reference: image "…" rejected: …); a volume that names no reference is not checked. See Image volumes below.A value the API server would replace with its default: the Go type omits a zero there, so the authored value cannot be carried (
containers[0].ports[0].protocol: "" cannot be carried by the Kubernetes API types (the field is omitted when zero, so the API server would apply its default "TCP")). These are:- written as
0: a probe’stimeoutSeconds,periodSeconds,successThresholdorfailureThreshold(defaults 1, 10, 1, 3), on the liveness, readiness and startup probes of every init and regular container; - written as
"":dnsPolicy(ClusterFirst),restartPolicy(Always) andschedulerName(default-scheduler); on every init and regular containerimagePullPolicy,terminationMessagePath(/dev/termination-log),terminationMessagePolicy(File), a port’sprotocol(TCP), anenventry’svalueFrom.fieldRef.apiVersion(v1), and thehttpGetpath(/) andscheme(HTTP) of its probes and of itspostStartandpreStophooks; on a volume, thefieldRef.apiVersionof adownwardAPIitem, also inside aprojectedsource (v1), an image volume’spullPolicy,iscsi.iscsiInterface(default),rbd.pool,rbd.userandrbd.keyring(rbd,admin,/etc/ceph/keyring) andscaleIO.storageModeandscaleIO.fsType(ThinProvisioned,xfs). AnimagePullPolicyor image volumepullPolicydefaults toAlwaysfor an image taggedlatestor naming neither a tag nor a digest, else toIfNotPresent; the image rule refuses the first two, so an accepted image getsIfNotPresent. The code defaults an image volume’spullPolicyonly under theImageVolumefeature gate, which is on and locked since Kubernetes 1.36; - written as
0whenhostNetworkistrue: a container port’shostPort, which the API server sets to the port’scontainerPort. It does so on the Pod, so on a template it applies to each pod created from it.
Two strings the API server sets are not refused.
serviceAccountNameand its deprecated spellingserviceAccountare kept equal, so an authored""with neither set stays""; thedefaultservice account is set by an admission plugin, not by defaulting. A string field that is a pointer in the Go type (a gRPC probe’sservice, anhttpGetprotocol, a container’srestartPolicy, ahostPathtype,azureDisk’s fields) carries an authored"", so nothing is lost there.The list is held to the API server’s own defaulting code by
TestKubernetesDefaulters_MatchVendoredSource. It reads an excerpt of that code, copied unmodified from kubernetes/kubernetes at the release of the linkedk8s.io/apiunder its Apache-2.0 licence, intestdata/upstream/kubernetes/(see the next paragraph). The test walks the generatedSetObjectDefaults_function of the Pod, PodTemplate, ReplicationController and ReplicaSet objects and every defaulting function they call. It fails on a field the code defaults and the list does not hold, on a default the list states otherwise, and on a row the code does not set. The walk is closed: it accepts only the kinds of statement, expression and type, the unary operators and the calls the excerpt at the vendored tag uses, each listed in the test (a method by its receiver’s identity, a builtin only while no name of the excerpt shadows it), and fails on anything else, naming it and its position, rather than guess. It also fails on listed shapes it cannot follow: among others, a write through a copy of a field (read by name or through a dereference), a list element reached by anything but a range over that list, abreakout of a loop, a method called on the object, a variable shadowing one of an enclosing block, or a write it cannot place in the object. The trade-off is that a re-vendoring whose code uses a new shape or call fails the test until that shape is understood and listed, even when it changes no default. Most defaults are set by aSetDefaults_function. The exceptions are a port’sprotocoland theiscsi,rbdandscaleIOstrings, whichk8s.io/apideclares with a+defaultmarker and the generated functions apply, andhostPort, whichdefaultHostNetworkPortssets. A second test holds the numbers to the field comments of the linked types.The excerpt in
testdata/upstream/kubernetes/holds eight files of kubernetes/kubernetes under its Apache-2.0 licence, each with its header, and kubernetes/kubernetes’sLICENSE, the licence’s full text, at the same tag. ItsSOURCEfile names the tag and the git blob id of each file and ofLICENSE; the tests check each against its file, the tag against the linkedk8s.io/api, and fail whenLICENSEis missing.mise run vendor-k8s-defaulters vX.Y.Zre-fetches it when that module moves. CI never runs it, and the tests need no network.- written as
A match expression without its
keyoroperator, with an operator that is none of the four, or withvaluesthat do not go with the operator, in every label selector of the spec: a pod affinity or anti-affinity term’slabelSelectorandnamespaceSelector, a topology spread constraint’slabelSelector, a projectedclusterTrustBundlesource’slabelSelectorand a generic ephemeral volume’s claimselector(see “A label selector’s match expressions”). A node selector term is not a label selector and is left to the API server.
Marked required upstream and not checked: the
actionof a container restart rule and theoperatorof its exit codes (restartPolicyRules[]), and thesignerNameandkeyTypeof a pod certificate source of a projected volume. The Go type leaves each out when it is empty, so the Pod would show the omission, and launcher does not refuse it: the built-in types ship markers and no schema, so no linked module shows that the API server refuses a Pod without one, and both fields are behind a feature gate of the cluster.TestKindComponents_OmittedRequiredAndWrittenDefaultslists the four with that reason.Policy.
ApplyPolicyruns the check the rendered-object check runs on a Pod (enforcePodTemplatePolicy): host namespaces, hostPath volumes, the storage maximum on a generic ephemeral volume’s claim, the registry allowlist on an image volume’s reference, the pod-level cpu and memory maxima, and for every init and regular container the registry allowlist, the cpu and memory maxima and the privileged, HostProcess and capability gates. It fills no policy default: a container without resources stays without. The kind names a field as the property it is (hostNetwork is not allowed by environment policy,containers[0] "app": …); the three rendered paths name it under the object (passthrough: object Pod "runner": spec.containers[0] "app": …).Image volumes (go-kure/launcher#790). An image volume (
volumes[].image) mounts an OCI image the kubelet pulls as it pulls a container’s, so itsreferenceis held to the allowed registries (AllowedRegistries), refused with the registry class:volume "ext" image.reference: image "other.example/team/ext:1.0.0" is not from an allowed registry [registry.example]. The reference is read as a container image is: one that names no registry host is Docker Hub’s (docker.io), a tag or a digest is not part of the host, and the host must equal a list entry exactly, a trailing/on the entry aside (registry.example:5000is notregistry.example). A volume that names no reference is not checked, and no list, or an empty one, allows every registry. The one check holds it on every path by which a document brings a pod spec: this kind, the pod template ofreplicaset,replicationcontroller,podtemplateandcnpg-pooler, and the pod spec of a workload that template delivery (helmtemplate),passthroughor amanifestssource carries. The components with a volume schema of their own (webservice,worker,deployment,statefulset,daemonset,job,cronjob) take no image volume: theirvolumeshas no such type. With the container images these are every field of a pod spec that names an image the kubelet pulls today. A test walks the linkedk8s.io/apitype and fails on a field whose name holdsimage, or whose type is the image volume source, that is neither held nor listed with a reason; an image field under another name and another type is not found by it. Not covered: a custom resource thatpassthrough, amanifestssource or a chart carries passes whatever it holds, the pod template of a CloudNativePG Pooler and the extension images of a Cluster included; only the kind that builds the object (cnpg-pooler,cnpg-cluster) holds its fields. Breaking: an image volume naming a registry outside the list built before, on each of those paths, and is refused now.The tag rule on an image the document names. The reference of an image volume is also held to
ValidateImageRef, as a container’s image is: one without a tag or digest, or tagged:latest(with a digest too), or one that is no image reference, is refused, on each of the paths above (volumes[0] "ext" image.reference: image "registry.example/team/ext" rejected: no tag or digest specified; use an explicit version tag or digest; a rendered path names it under the object,spec.template.spec.volumes[0] "ext" …). The rule is the library’s, not the environment’s: the kinds apply it when the component is read and again at generation, with or without a policy, and the refusal carries no policy class. A reference by digest alone passes, and a volume that names no reference is not checked. The kinds that name an image outside a pod spec hold it to the same rule:cnpg-pooleritspgbouncer.imageand the images of its template,cnpg-clusteritsimageNameand the reference of each extension,postgresqlthe image it composes fromversion(each in its entry). The test that walks the types fails on a field held to the registry rule and not to this one. Breaking (go-kure/launcher#790): a document, a chart or a source naming an untagged or:latestimage in one of those fields built before, and is refused now. The two catalog kinds (cnpg-imagecatalog,cnpg-clusterimagecatalog) hold each image, component image and extension reference of a catalog to the rule from the release that adds them, so nothing that built is refused there.Known difference: template delivery (
helmtemplate),passthroughandmanifestsdo not refusepriorityoroverheadon a Pod they emit; the admission controllers decide there. They do refuse ephemeral containers and an untagged or:latestimage, an image volume’s included.The config implements
oam.ServiceAccountNamer: the account is the authoredserviceAccountName, or the deprecatedserviceAccountwhere that one is unset, as the API server reads the two; with neither, the namespace’sdefaultaccount. The Pod carries theapplabel and the component label, and whatlabelsandannotationsauthor beside them.replicaset (go-kure/launcher#790) is the kind-named projection of an apps/v1 ReplicaSet, on the same recipe: one schema key per json field of
appsv1.ReplicaSetSpec(replicas,minReadySeconds,selector,template), the property map decoded strictly into that type, and one ReplicaSet named after the component in the build namespace, with the authored spec. It is the bare controller: no rollout, which adeploymentcomponent gives, and none of that kind’s shorthand — a property of the workload kinds (image,ports,strategy) is refused as not a ReplicaSetSpec field.The
applabel. The handler adds one thing:app: <component>on the pod template’s labels, beside the authored ones (see “Theapplabel”). It is the label every workload kind gives its pods and the one launcher’s traits and Services select on, so anetworkpolicytrait’s policy and aservicecomponent withselector: {app: <component>}reach the pods. The selector is emitted as authored, and the ReplicaSet itself gets no label of the handler’s; the transform then sets the component label on the object and its template, as on every object a component owns (go-kure/launcher#788).- An authored
template.metadata.labels.appwith the component’s own label value is kept. Any other value is refused, naming the component (template.metadata.labels.app: "frontend" is not the `app` label of component "web" ("web")): the label cannot say something else than what the traits select. - A selector that matches the authored template labels and would stop
matching once the label is added (
appunderDoesNotExist, orNotInthe label value) is refused (selector: rules out the label `app: web` …). A selector onapp: <component>is satisfied by the added label, so the template needs no label of its own for it. A selector that cannot be read as one (an expression without itskeyoroperator, an unknown operator,valuesthat do not go with the operator, a key or value that is no label) cannot be compared and is refused with apimachinery’s reason (selector: …). That covers what “A label selector’s match expressions” lists, so this selector is not read a second time.
Traits. A trait target as the
podkind is:security-context, aconfigmaptrait’smountPathand anexternal-secrettrait’senvFrom/mountPathchange its pod template.topology-spreadandscalerare not available on it.Refused when the component is read, with or without a policy:
- An unauthored
selector. The Go type cannot omit it, so it would be written asnull, and launcher derives none. Whether it matches the template’s labels, like the API’s other value rules (an empty selector,restartPolicyother thanAlways, a negativereplicas), is left to the API server. - Under
template.spec, what thepodkind refuses of its own spec, by the same checks and named by path (template.spec.containers: required,template.spec.priority: not authorable …, the image rule, a probe timing written as0, a defaulted string written as"", ahostPortof0underhostNetwork, a defective match expression in a label selector of the pod spec). A test holds the probe list to every non-pointeromitemptynumber or boolean underReplicaSetSpecwhose field comment states a non-zero default, the template’s metadata included, andTestKubernetesDefaulters_MatchVendoredSourceholds the whole list to the API server’s defaulting code for a ReplicaSet. template.spec.activeDeadlineSeconds. The API server forbids it on a ReplicaSet’s pod template, whose pods are replaced for as long as the controller exists; apodor a Job may set it.
The two restart-rule fields and the two pod-certificate fields that are marked required upstream are not checked under
template.speceither, for the reason thepodkind gives: no linked module shows that the API server refuses the omission.Policy.
ApplyPolicyholdsreplicasto the replica maximum, an unset one as the 1 the API server defaults it to (replicas 4 exceeds enforced maximum 3), and the pod template to the check thepodkind runs (enforcePodTemplatePolicy), naming a field by its path in the properties (template.spec: hostNetwork is not allowed by environment policy,template.spec.containers[0] "app": …). It fills no policy default: an unsetreplicasstays unset and a container without resources stays without. The three rendered paths run the same checks on a ReplicaSet they emit and name the field under the object (spec.replicas,spec.template.spec…).Known difference: template delivery (
helmtemplate),passthroughandmanifestsdo not refusepriority,overheadoractiveDeadlineSecondson a ReplicaSet they emit, do not require a selector and add noapplabel; the API server decides there. They do refuse ephemeral containers and an untagged or:latestimage, an image volume’s included.The config implements
oam.ServiceAccountNamer, reading the template’sserviceAccountNameas thepodkind reads its own. The ReplicaSet’s own labels and annotations are thelabelsandannotationsproperties; the template’s metadata is carried as authored and takes neither.- An authored
replicationcontroller (go-kure/launcher#790) is the kind-named projection of a v1 ReplicationController, on the
replicasetkind’s recipe: one schema key per json field ofcorev1.ReplicationControllerSpec(replicas,minReadySeconds,selector,template), the property map decoded strictly into that type, and one ReplicationController named after the component in the build namespace, with the authored spec. What thereplicasetkind states holds here — theapplabel on the pod template and the refusal of an authored one with another value, the three traits, the refusals undertemplate.spec(activeDeadlineSecondsincluded), the two restart-rule fields and the two pod-certificate fields that are marked required upstream and are not checked, because no linked module shows that the API server refuses the omission, the replica maximum and the pod-template policy check, no policy default, the known difference of the three rendered paths,oam.ServiceAccountNamer, andlabelsandannotationsfor the object’s own metadata — with these differences:selectoris a plain map of label to value, not a label selector: amatchLabelsormatchExpressionskey is refused as not a ReplicationControllerSpec field.selectoris optional. An unset one stays unset, and the API server defaults it to the pod template’s labels, theapplabel included.templateis required (template: required). The API type holds it by pointer, so an unauthored one would be emitted without pods to create.- No selector is refused against the
applabel: a map selector asks only for labels to be present, which a further label on the pods cannot break.
podtemplate (go-kure/launcher#790) is the kind-named projection of a v1 PodTemplate. The object has no spec: beside its identity it holds one field,
template, and that is the component’s one property, decoded strictly intocorev1.PodTemplateSpec. It emits one PodTemplate named after the component in the build namespace, with the authored template. Any other property is refused, the object’s ownkind,apiVersionandmetadataincluded (properties do not decode into a v1 PodTemplate …).It is stored, not run. No controller creates pods from a PodTemplate, so the handler adds no
applabel to the template (an authoredapplabel is carried as written), the config reports no ServiceAccount (oam.ServiceAccountNameris not implemented, so anrbactrait binds the component name), and it is not a trait target: aconfigmaptrait’smountPath, anexternal-secrettrait’senvFrom/mountPathand asecurity-contexttrait’s pod-spec properties (runAsUserand the five others) are refused on it, naming the component and the kinds they apply to.security-contextwithpsaLevelalone is accepted, as a declaration of the level, and writes nothing to the template (go-kure/launcher#794, item 14;pkg/oam/builtin/traits/README.md, “Pod-spec traits on a component without a workload”). The transform still sets the component label on the object and its template, as on every object a component owns (go-kure/launcher#788).Refused when the component is read, with or without a policy: under
template.spec, what thepodkind refuses of its own spec, by the same checks and named by path (template.spec.containers: required, which an unauthoredtemplatealso gives;template.spec.priority: not authorable …; the image rule; a probe timing written as0; a defaulted string written as""; ahostPortof0underhostNetwork, which the API server would replace in each pod created from the template; a defective match expression in a label selector of the pod spec).template.spec.activeDeadlineSecondsis allowed, unlike on areplicasetorreplicationcontroller: the API server accepts it on a PodTemplate. The two restart-rule fields and the two pod-certificate fields that are marked required upstream are not checked, for the reason thepodkind gives: no linked module shows that the API server refuses the omission. The API’s other value rules are left to the API server.Policy.
ApplyPolicyholds the template to the check thepodkind runs (enforcePodTemplatePolicy), naming a field by its path in the properties (template.spec: hostNetwork is not allowed by environment policy), since whatever reads the template creates pods from it. It fills no policy default. The three rendered paths run the same check on a PodTemplate they emit and name the field the same way (template.spec…).Known difference: template delivery (
helmtemplate),passthroughandmanifestsdo not refusepriorityoroverheadon a PodTemplate they emit; the API server decides there. They do refuse ephemeral containers and an untagged or:latestimage, an image volume’s included. The PodTemplate’s own labels and annotations are thelabelsandannotationsproperties; the template’s metadata is carried as authored and takes neither.storageclass, volumeattributesclass, priorityclass, runtimeclass, ingressclass, csidriver (go-kure/launcher#790) are the kind-named projections of six cluster-scoped objects: a
storage.k8s.io/v1StorageClass, VolumeAttributesClass and CSIDriver, ascheduling.k8s.io/v1PriorityClass, anode.k8s.io/v1RuntimeClass and anetworking.k8s.io/v1IngressClass. Each emits that one object, named after the component unlessobjectNamenames it, with no namespace, holding exactly what was authored: the handler adds no label, no annotation and no default of its own, and the transform sets the component label, as on every object a component owns (go-kure/launcher#788).One shared helper builds all six (
policyFreeKind, inkind_policy_free.go), for a kind to which no dimension of the environment policy applies;servicecidr,poddisruptionbudget,endpointslice, the four kinds of the RBAC API, the four of the Prometheus operator’s API, the four of Cilium’s BGP control plane and the five kinds of the Gateway API’s infrastructure objects are built on it too. The three kinds of cert-manager’s API, the two of VolSync’s,alertmanager,thanosrulerandprometheusare built onpolicyHeldKind(kind_policy_held.go): this helper, unchanged, with anApplyPolicythat asks one function of the kind whether the policy refuses the decoded value. It refuses or passes; it fills no default. A kind is a value of it naming the upstream type, an optional check of required fields, an optional list of required fields the type writes whether or not they were authored, and the base-library constructor; the helper is the rest: the strict decode of the property map into the upstream type under the package’s null contract, which refuses anulllist element by its path (decodeKindSpec), the refusal of a key written in two spellings (refuseUncarriedSpecValues), the refusal of a listed required field that was not authored (refuseUnauthoredRequired, which reads the authored properties, since the decoded value does not show the omission, and follows a list to each of its items), a config whoseApplyPolicydoes nothing, and aGeneratethat returns the constructor’s object, under the object name the config carries, with a deep copy of what was decoded, so two builds of one config share nothing. The config type is unexported: a component is only built from properties that went through the decode. A kind may list its defaulted zeros (defaultedZeros), and an authored0,falseor""on a listed field is refused, as the secret-store kinds do (see the secretstore entry) andalertmanagerdoes for the probes of the containers it lists and three strings of its own. A kind that lists none suits a type only when none of its omit-when-zero numbers or booleans has a non-zero API default;TestPolicyFreeKinds_NoDefaultedZerosreads the field comments of every Kubernetes type built on it and fails on one whose comment states such a default in a form it recognises (Defaults to 1,Default is true). A default the comment words otherwise, or does not state, is not found, and a string’s default is not read from a comment. The types of the Prometheus operator’s API publish no field comment;TestMonitoringKinds_DefaultedZerosreads the default markers of their source instead, strings included. Cilium’s publish none either, and its module ships the CRDs:TestCiliumBGPKinds_DefaultsSitOnPointersholds every default of the four BGP CRDs to a field that is a pointer in the Go type.Authored empty lists the API defaults. A defaulted-zero list may also hold a list the type omits when empty, and an authored
[]there is refused the same way (metrics: [] cannot be carried by the Kubernetes API types (the field is omitted when empty, so the API server would apply its default …)). Three lists are refused: ahorizontalpodautoscaler’smetricsand its scaling rules’policies, and anetworkpolicy’spolicyTypes. Two lists the API defaults are not refused, because an empty list already means their default:csidriver’svolumeLifecycleModes(an empty list meansPersistent) andimagerepository’sexclusionList(the image-reflector controller filters tags throughGetExclusionList, which returns the default list when the list is empty, so an API client’s[]means the default too).TestKindComponents_DefaultedEmptyListswalks every kind in its table of API sources, finds each list omitted when empty that its source gives a default (a CRD schema or a default marker) or whose field comment mentions a default, and holds each to an answer: refused, or not refused with the reason. A newly defaulted list fails it until it is answered. The three refused defaults are held to the API server’s defaulting code byTestKubernetesDefaulters_ListDefaultsMatchVendoredSource, from the excerpt thepodkind’s list is held to: the functions that reach those defaults are held to their exact statements, and each default to being the only write to its field or to anything beneath it. Two limits: the lists of the Kubernetes types are found from their field comments, so a list the API server defaults without its comment saying so is not found; and the table of kinds it walks is not itself proven to hold every kind component.What is authored.
ingressclassandcsidriverhave a spec type, and the properties are its json fields:controllerandparameters(IngressClassSpec), and the eleven fields ofCSIDriverSpec(attachRequired,podInfoOnMount,volumeLifecycleModes,storageCapacity,fsGroupPolicy,tokenRequests,requiresRepublish,seLinuxMount,nodeAllocatableUpdatePeriodSeconds,serviceAccountTokenInSecrets,preventPodSchedulingIfMissing). Acsidriverrequires no property; aningressclassrequirescontroller(see Required below).- The four classes have none: their fields sit on the object, beside its
identity. The properties are those fields, decoded strictly into the
object type, and the object’s own
kind,apiVersionandmetadataare refused by name under any spelling the decoder would match (metadata: not authorable: launcher sets the object's kind, apiVersion and metadata (its name is the component's, or the one objectName gives it; its labels and annotations are the labels and annotations properties)).storageclass:provisioner,parameters,reclaimPolicy,mountOptions,allowVolumeExpansion,volumeBindingMode,allowedTopologies.volumeattributesclass:driverName,parameters.priorityclass:value,globalDefault,description,preemptionPolicy.runtimeclass:handler,overhead,scheduling. - Required is a top-level field the API server refuses an object
without:
provisioner(storageclass),handler(runtimeclass),driverName(volumeattributesclass) andcontroller(ingressclass) must be a non-empty string (provisioner: required …), and avolumeattributesclassmust carry at least one ofparameters(parameters: required …). The upstream type markscontrolleroptional; the API server’s validation requires it all the same. The API does not require a PriorityClassvalue: the type always encodes one, so apriorityclassthat authors none is emitted withvalue: 0. Every other value rule (which reclaim policies exist, a required field of a nested object, the names a cluster-scoped object may carry) is left to the API server. - A quantity written as a number is emitted in its canonical string form.
An authored
falseor0is kept where the API tells it from an unset field (allowVolumeExpansion: false,attachRequired: false,value: 0). Where the API type omits a zero, the field is left out of the object, which the API reads as the same value:globalDefault: falseand an emptydescription.
The object’s name is the component’s unless
objectName, or theNaminghook under roleobject, names it otherwise (see “The object name” below). Each handler declares its object as cluster-scoped, so the name is claimed in no namespace: twostorageclasscomponents given one object name are refused, whatever namespace the application is built for. What refers to the object names it by its object name: a claim’sstorageClassName, a pod’sruntimeClassNameorpriorityClassName. The API identifies a CSI driver by the CSIDriver’s name, so acsidrivercomponent’s object name must be the driver’s (csi.example.com), written as the component name or asobjectName. The build namespace does not apply to these objects. The handlers check no name rule of their own on either name: one the API server refuses for the kind builds here and is refused at apply. A CSIDriver’s name is not held to the limit the API documents for it. The type’s documentation gives a driver name at most 63 characters; the API server does not hold the CSIDriver object to that, and accepts any DNS subdomain name. It does refuse a PersistentVolume that names a driver longer than 63 characters, so such a CSIDriver is created and no volume can use it. Thecsidriverkind refuses what the API server refuses of the object and no more, so it builds that name too.Policy. None of the six has a field an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing, and each builds the same under every policy and under none. A consumer that does not want an author to create one of these objects does not register its handler; the transform then refuses the component (no handler for component type …).Labels and annotations are the
labelsandannotationsproperties, on all six. That includes the annotations that mark a default class (storageclass.kubernetes.io/is-default-class,ingressclass.kubernetes.io/is-default-class): a default StorageClass or IngressClass writes its annotation there. Whether a second default exists is the cluster’s to decide.Not covered.
- An
ingressclasswhoseparametersleavesscopeout is emitted withscope: null: the API type always encodes the field, and documentsClusteras its default. Authorscopeto emit a value. - Whether a referenced object exists: the
runtimeclassa pod names, thestorageclassa claim names, the parameters object of aningressclass.
ingress (go-kure/launcher#790) is the kind-named projection of a networking.k8s.io/v1 Ingress, on the recipe of
resourcequota: one schema key per json field ofnetworkingv1.IngressSpec(ingressClassName,defaultBackend,tls,rules), the property map decoded strictly into that type under the null contract, two spellings of one field refused, andTestCoreKindSchemas_CoverSpecholding the schema to the linked type. It emits one Ingress named after the component in the build namespace, with the authored spec and nothing else. No field is required by the decode and none is filled: an Ingress with neither adefaultBackendnor a rule, a path without apathTypeand the API’s other value rules are left to the API server. A null list element (rules: [null], a null path or TLS entry) is refused by its path.It is an authored object, not the
ingresstrait, although the two share the type name. The trait attaches to a component and routes to that component’s Service; this kind is a component of its own, and a backend is a Service reference carried as written. Three things the trait has do not reach it:- No NetworkPolicy allow rule. The NetworkPolicy synthesis opens a
backend’s port to the ingress controller from what a routing trait reports
(the traffic sources capability rendering gives it, and the component it
targets). This kind reports neither, so the synthesis reads nothing from
it and allows nothing for it, whatever Service it names. An author who
wants the allow rule puts the
exposeoringresstrait on the backend component, or authors the NetworkPolicy (thenetworkpolicytrait or kind). - No hostname constraint. The platform’s
allowedHostnameWildcard, which the trait holds every hostname to, is a trait rendering input. A host authored here is not checked against it. - No environment policy.
ApplyPolicyis a no-op: the policy has no rule for an Ingress, and its capability lists (AllowedCapabilities,ForbiddenCapabilities,RequiredCapabilities) gate trait types, so a policy that forbids theingresstrait does not refuse aningresscomponent.passthrough,manifestsand template delivery emit an Ingress under the same terms. A consumer that restricts routing restricts the component types it registers.
The name is the component’s, or its
objectName(“The object name” below). Aningresstrait names its own Ingress (<component>-ingress, or itsname); a component and a trait whose Ingress would carry one name are refused, with both named.Labels and annotations are the
labelsandannotationsproperties, a controller’snginx.ingress.kubernetes.io/…annotations among them. Not covered: itsstatus, which the controller writes.- No NetworkPolicy allow rule. The NetworkPolicy synthesis opens a
backend’s port to the ingress controller from what a routing trait reports
(the traffic sources capability rendering gives it, and the component it
targets). This kind reports neither, so the synthesis reads nothing from
it and allows nothing for it, whatever Service it names. An author who
wants the allow rule puts the
httproute (go-kure/launcher#790) is the kind-named projection of a gateway.networking.k8s.io/v1 HTTPRoute, on the same recipe as
ingress: one schema key per json field ofgatewayv1.HTTPRouteSpec, those of theCommonRouteSpecit inlines included (parentRefs,useDefaultGateways,hostnames,rules), decoded strictly into that type. No type underHTTPRouteSpecunmarshals itself, so an unknown key is refused at every depth. It emits one HTTPRoute named after the component in the build namespace, with the authored spec and nothing else. No field is required by the decode and none is filled; the API’s value rules (a filter’stypematching the member set, a rule’smatchesandbackendRefslimits) are left to the API server. A null list element (rules: [null], a null parent or backendRef) is refused by its path.useDefaultGatewaysis a field of the Gateway API’s experimental channel: the standard channel’s HTTPRoute CRD does not hold it, so a cluster on that channel does not keep it.It is an authored object, not the
httproutetrait, although the two share the type name. The trait attaches to a component and routes to that component’s Service; this kind is a component of its own, and a backendRef is a reference carried as written, itsnamespaceincluded. Three things the trait has do not reach it:- No NetworkPolicy allow rule. As for
ingress: the NetworkPolicy synthesis reads a routing trait’s traffic sources and target component, which this kind does not report, so it allows nothing for the route’s backends. An author who wants the allow rule puts theexposeorhttproutetrait on the backend component, or authors the NetworkPolicy. - No parent from a capability. The trait builds
parentRefsfrom the Gateway its capability rendering names when the author writes none. HereparentRefsis what the author wrote; unwritten, the route has no parent. - No environment policy.
ApplyPolicyis a no-op, and the policy’s capability lists gate trait types, so a policy that forbids thehttproutetrait does not refuse anhttproutecomponent.passthrough,manifestsand template delivery emit an HTTPRoute under the same terms. A consumer that restricts routing restricts the component types it registers.
The name is the component’s, or its
objectName. Anhttproutetrait names its own HTTPRoute (<component>-httproute, or itsname); a component and a trait whose HTTPRoute would carry one name are refused, with both named.Labels and annotations are the
labelsandannotationsproperties. Not covered: itsstatus, which the Gateway controller writes.- No NetworkPolicy allow rule. As for
grpcroute (go-kure/launcher#790) is the kind-named projection of a gateway.networking.k8s.io/v1 GRPCRoute, on the
httproutekind’s recipe and not onpolicyFreeKindas the TCP, UDP and TLS routes are: one schema key per json field ofgatewayv1.GRPCRouteSpec(parentRefs,useDefaultGateways,hostnames,rules), decoded strictly into that type, and one GRPCRoute emitted in the build namespace, named after the component unlessobjectNamenames it. The API requires no rule, backend or hostname of a GRPCRoute, as of an HTTPRoute, so the decode requires none and fills none; the API’s value rules are left to the API server. A null list element is refused by its path. Parent and backend references are carried as written, theirnamespaceincluded. As forhttproute, no NetworkPolicy allow rule is synthesized for its backends, no parent comes from a capability, andApplyPolicyis a no-op. Its required fields written unauthored and not refused are listed under Required fields a kind writes unauthored and does not refuse.Labels and annotations are the
labelsandannotationsproperties. Not covered: itsstatus, which the Gateway controller writes.networkpolicy (go-kure/launcher#790) is the kind-named projection of a networking.k8s.io/v1 NetworkPolicy, on the same recipe as
ingress: one schema key per json field ofnetworkingv1.NetworkPolicySpec(podSelector,ingress,egress,policyTypes), decoded strictly into that type. It emits one NetworkPolicy named after the component in the build namespace, with the authored spec and nothing else. No top-level field is required and none is filled. A match expression ofpodSelector, or of a peer’spodSelectorornamespaceSelector, is refused without itskeyoroperator, with an operator that is none of the four, or withvaluesthat do not go with the operator (see “A label selector’s match expressions”); the config is exported, soGeneraterepeats what the typed spec can show. The API’s other value rules (a peer naming at least one ofpodSelector,namespaceSelectorandipBlock, anipBlockbeside no selector, a valid CIDR, a label key’s syntax) are left to the API server.The spec reads as the API reads it, which is not how the
networkpolicytrait reads the same keys. The two share the type name; the trait attaches to a component and scopes the policy to that component’s pods, and this kind is a component of its own that scopes nothing:podSelectoris not defaulted. The trait has no such property: it always selects its component’s pods. Here an unwrittenpodSelector, like{}, is the empty selector: the policy applies to every pod of the namespace. An author moving a policy from the trait to the kind writes the selector out.policyTypesis not derived. The trait has no such property either: it lists a direction when that direction’s key is present, so itsegress: []denies all egress. HerepolicyTypesis what the author wrote; unwritten, the API server derives it:Ingressalways,Egressonly whenegressholds a rule. So on the kindegress: []alone isolates no egress; a policy that denies all egress writespolicyTypes: [Egress]. An authoredpolicyTypes: []is refused: the type omits it, and the API server would derive it as above (policyTypes: [] cannot be carried by the Kubernetes API types (…)).- An empty rule allows everything in its direction.
ingress: [{}]is the API’s allow-all, and is carried when written. It is never the result of a null: a null rule, peer or port (ingress: [null],from: [null]) is refused by its path, since decoded it would be the empty, allow-all one. An emptyingressoregresslist is carried as an unwritten one, which the API reads the same way. - A selector picks pods by label. A peer’s
podSelectoris not a component reference and nothing resolves it; the label a workload component’s pods carry is described under “Theapplabel” below. - No environment policy.
ApplyPolicyis a no-op, and the policy’s capability lists gate trait types, so a policy that forbids thenetworkpolicytrait does not refuse anetworkpolicycomponent.passthrough,manifestsand template delivery emit a NetworkPolicy under the same terms. A consumer that restricts network policy restricts the component types it registers.
The transform’s NetworkPolicy synthesis does not read this kind: it neither counts an authored policy as covering a component nor merges into it, and the policies it emits (
{comp}-allow-ingress-trafficand the others) are additive beside it. The name is the component’s, or itsobjectName; a component whose policy would carry the name of anetworkpolicytrait’s (<component>-allow, or itsname) or of a synthesized one is refused, with both named. The NetworkPolicy’s own labels and annotations are thelabelsandannotationsproperties.cilium-networkpolicy (go-kure/launcher#790) is the kind-named projection of a cilium.io/v2 CiliumNetworkPolicy. The object has no spec type of its own: it holds one rule under
spec, a list of rules underspecs, or both, each a Ciliumapi.Rule. The component’s properties are those two fields, decoded strictly into that type under the null contract, andTestCoreKindSchemas_CoverSpecholds the two keys to the linkedCiliumNetworkPolicytype. It emits one CiliumNetworkPolicy named after the component in the build namespace, with the authored rules and nothing else. The whole rule is authorable:endpointSelector,ingress,ingressDeny,egress,egressDeny,labels,enableDefaultDeny,description,log.Refused at build, because Cilium rejects the policy. Its CRD schema refuses some of these at admission (a rule with no selector, or without any of the four rule keys); the rest the API server stores and the agent rejects when it reads the object, so that nothing enforces it. Launcher refuses all of them instead of emitting the object:
- no rule at all: neither
specnor an entry inspecs. An emptyspecsholds none; beside aspecit is accepted and not emitted; - a rule with no
endpointSelector. None is filled in:{}selects every endpoint of the namespace and is carried as written, and a null or absent one is refused; - a rule with a
nodeSelector, which belongs to a CiliumClusterwideNetworkPolicy and which Cilium rejects in a namespaced policy; - a rule with no entry in any of
ingress,ingressDeny,egressandegressDeny. A deny list counts as much as an allow list; a null or empty list holds no entry.
These are the checks Cilium’s own reader makes that need no agent configuration; the code names the Cilium functions they mirror. The rest of a rule’s value rules (a port’s range, an entity’s name, which peers may be combined) are left to Cilium. A null list element (
specs: [null], a null ingress entry or peer) is refused by its path.Required. Inside a rule the CRD requires 59 fields that the Cilium type writes whether or not they were authored, so that the object would not show the omission: a match expression without its
operatorwas emitted asoperator: "", anauthentication: {}asmode: "", aterminatingTLS: {}assecret: null, each of which the API server refuses. Each is refused where its parent is authored, by its path (spec.ingress[0].authentication.mode: required (…)):- the
keyandoperatorof a match expression, in every selector of a rule (endpointSelector,fromEndpoints,toEndpoints,fromNodes,toNodes, a CIDR entry’scidrGroupSelector, ak8sServiceSelector’sselector), under the deny lists too; - a
k8sServiceSelector’sselector, thekeyof a rule label and thetypeof anicmpsfield; - under an allow list only: an
authentication’smode; a port’slistenername, itsenvoyConfigand that configuration’sname; thesecretof a port’sterminatingTLSandoriginatingTLSand that Secret’sname; an HTTP header match’sname, and thenameof itssecretwhere one is authored.
Two more are optional to the API and required here: a listener’s
priorityand thekindof itsenvoyConfig. The type writes an unauthored one as0and as the empty string, and the API refuses both (a priority is 1 to 100, a kind isCiliumEnvoyConfigorCiliumClusterwideEnvoyConfig), so a listener that leaves one out cannot be emitted.TestCiliumNetworkPolicy_RequiredMatchCRDholds the whole list to the CiliumNetworkPolicy CRD of the linked module, its own file and not the cluster-wide policy’s, and to the rule type, underspecand underspecs, so a dependency bump that adds, drops or moves one fails there. The CRD names anodeSelector’s match expressions as well, and so does the list: an incomplete one is refused by this rule, a complete one by the rule above. A rule label without itskeyis refused earlier, by Cilium’s own decoding, as anicmpsfield without itstypeis (below). An authored empty value in a required field is not checked: it is the API server’s to refuse. The list of a rule is held in the internalpkg/oam/internal/requiredfields(CiliumRule), since thecilium-networkpolicytrait reads it too, cut to the three fields of a rule it publishes.An unknown key inside a selector is refused, by its path (
spec.ingress[0].fromEndpoints[1].matchLabel: unknown field). Cilium’s endpoint selector unmarshals itself and drops a key it does not know, which the strict decode cannot see:endpointSelector: {matchLabel: {...}}would be the empty selector, which selects every endpoint of the namespace, and a peer misspelt the same way would match every endpoint. The check covers every position that holds a selector (endpointSelector,fromEndpoints,toEndpoints,fromNodes,toNodes, a CIDR entry’scidrGroupSelector, under the deny lists too) and the two other types under a rule that unmarshal themselves: anicmpsfield (family,type) and an entry oflabelsin its object form (key,value,source). The positions are found from the Cilium types, and a test fails when a Cilium bump adds a type that unmarshals itself and is not checked.An
icmpsfield needs itstype. Cilium’s own decoding of the field panics whentypeis absent or null; the component is refused with an error that names the panic (… the decoder of … panicked on this value: …) and not the field’s position, so look for anicmpsfield without atype.It is an authored object, not the
cilium-networkpolicytrait, although the two share the type name. The trait attaches to a component and publishes one rule’sname,endpointSelector,ingressandegress; this kind is a component of its own, named after the component or by itsobjectName, and publishes the whole rule andspecs. A component and a trait whose policy would carry one name (the trait’sname) are refused, with both named. Neither fills a selector. No environment policy applies:ApplyPolicyis a no-op, and the policy’s capability lists gate trait types, so a policy that forbids thecilium-networkpolicytrait does not refuse acilium-networkpolicycomponent.passthrough,manifestsand template delivery emit a CiliumNetworkPolicy under the same terms. A consumer that restricts network policy restricts the component types it registers. No literal secret is checked: an HTTP header match’svalueis written into the object as authored and is not refused, since a header value cannot be told from a credential; a header match’ssecretrefers to a Secret by name and keeps the value out of the document. The object’s own labels and annotations are the top-levellabelsandannotationsproperties; alabelsunderspecor aspecsentry is the rule’s own field. Not covered: itsstatus, which the Cilium agent writes.- no rule at all: neither
servicecidr (go-kure/launcher#790) is the kind-named projection of a cluster-scoped
networking.k8s.io/v1ServiceCIDR: a range the API server assigns Service cluster IPs from, beside the one it was started with. It is built onpolicyFreeKind, as the six kinds of the storageclass entry are, and what that entry says of them holds here: one object, named after the component unlessobjectNamenames it, with no namespace, declared cluster-scoped, holding exactly what was authored, the same under every policy and under none.- Authored: the one field of
ServiceCIDRSpec,cidrs, a list of IP blocks in CIDR notation, decoded strictly. Required: at least one block (cidrs: required …); the API server refuses a ServiceCIDR without one. Every other value rule is left to the API server: that a block is a valid CIDR, that there are at most two, and that two are of different IP families. - Labels and annotations are the
labelsandannotationsproperties. - Not covered. The API server refuses a change to a block once the object exists; a ServiceCIDR of one block may only gain a second. Launcher does not compare a build with the cluster, so a changed block builds here and is refused at apply. The object’s status is the API server’s and is not written.
- Authored: the one field of
poddisruptionbudget (go-kure/launcher#790) is the kind-named projection of a
policy/v1PodDisruptionBudget, built onpolicyFreeKind. It emits that one object in the build namespace, named after the component unlessobjectNamenames it, holding exactly what was authored: the handler adds no label, no annotation and no default of its own. The handler declares its object as namespaced, so the object name is claimed in the object’s namespace.- Authored: the four fields of
PodDisruptionBudgetSpec, decoded strictly:minAvailableandmaxUnavailable(each a count or a percentage string such as"50%"),selector(a label selector) andunhealthyPodEvictionPolicy. A count is emitted as a number and a percentage as a string, and an authored0is kept (maxUnavailable: 0allows no voluntary eviction). - Required: no top-level field. The API server accepts a budget with an
empty spec. Inside
selector, a match expression is refused without itskeyoroperator, with an operator that is none of the four, or withvaluesthat do not go with the operator (see “A label selector’s match expressions”). The API’s other value rules are left to it, the one thatminAvailableandmaxUnavailableexclude each other included: a component that authors both builds here and is refused at apply. - The selector is the author’s. Launcher points it at no component: an
unauthored
selectorselects no pod, andselector: {}selects every pod of the namespace. To cover a workload component, select itsapplabel (matchLabels: {app: <component>}), which holds the component name up to 63 characters and its label projection beyond. - Beside the
scalertrait.scalerwithenablePDB: truederives a budget for its own workload, selector included, and refuses one that would block every eviction. This kind is the authored object and applies none of that: it is for a budget the trait does not express (the trait always writesminAvailable: 50%; here the count or percentage is the author’s, as aremaxUnavailable, an eviction policy and pods launcher does not own). - Policy. No field of the spec is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing. - Labels and annotations are the
labelsandannotationsproperties. - Not covered. Whether the selector matches any pod. The object’s status is the disruption controller’s and is not written.
- Authored: the four fields of
endpointslice (go-kure/launcher#790) is the kind-named projection of a
discovery.k8s.io/v1EndpointSlice, built onpolicyFreeKind. It emits that one object in the build namespace, named after the component unlessobjectNamenames it, holding exactly what was authored: the handler adds no label, no annotation and no default of its own. The handler declares its object as namespaced, so the object name is claimed in the object’s namespace.- Authored: an EndpointSlice has no spec, so the properties are the
object’s own top-level fields, decoded strictly at every depth:
addressType,endpointsandports. The object’skind,apiVersionandmetadataare launcher’s and are refused under any spelling. An authoredfalseis kept (conditions: {ready: false}), and an endpoint’sdeprecatedTopologyis written as authored, though the type documents that the v1 API ignores a write to it. - Its Service is a label, and the label is the author’s literal. A
slice belongs to a Service only through its
kubernetes.io/service-namelabel, authored underlabels. Launcher derives the label from nothing and points it at no component: it does not follow aservicecomponent’sobjectName, so write the name the Service object takes. A slice without the label builds and belongs to no Service. Aservicecomponent of a type other thanExternalNamealways carries a selector (the kind refuses an empty one, and anExternalNameService has no endpoints), so a Service whose endpoints are all written by hand comes from elsewhere: amanifestscomponent, or outside the document. - Required are the fields the API’s source marks required and the Go
type writes whether or not they were authored, so that the built object
would not show the omission:
addressType(addressType: required …), an endpoint’saddresses(endpoints[0].addresses: required …), and thenameof an entry of an endpoint’shints.forZonesorhints.forNodes.TestBuiltinMarkerKinds_RequiredMatchMarkersderives the list from the+requiredand+optionalmarkers of the linkedk8s.io/apimodule and fails on a dependency bump that changes it. The API marks no field of the kind required that the type leaves out when unauthored; the same test fails if one appears.TestKindComponents_OmittedRequiredAndWrittenDefaultsholds the kind in its table as well, from the same markers: to that, and to no default marked on a field the type writes unauthored. The check is one of presence: an authored empty value (addressType: "",addresses: []) builds and is the API server’s to refuse. - Emitted empty. The type writes three fields the API does not require
whether or not they were authored. A slice that authors no
endpointsor noportscarriesendpoints: nullorports: null, an authored empty list is written as[], and an endpoint that authors no condition carriesconditions: {}. A port that authors noprotocolor nonameis written without it, and the API server fillsTCPand the empty name. - Not artifact sources. The addresses of an endpoint, FQDNs included, are where traffic goes, not an artifact source, and none is held to the policy’s allowed registries.
- Policy. No field of the object is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing. - Labels and annotations are the
labelsandannotationsproperties. - Not covered. Every rule on the form of a value is left to the API
server: that an address is of the slice’s
addressType, the limits the type documents on how many endpoints, addresses, ports and hints a slice holds, and the names of ports. The type documentsaddressTypeas immutable. Launcher does not compare a build with the cluster, so a changed one builds here and is refused at apply. Whether a Service of the labelled name exists is not checked.
- Authored: an EndpointSlice has no spec, so the properties are the
object’s own top-level fields, decoded strictly at every depth:
role, rolebinding, clusterrole, clusterrolebinding (go-kure/launcher#790) are the kind-named projections of the four objects of the
rbac.authorization.k8s.io/v1API, built onpolicyFreeKind. Each emits that one object, named after the component unlessobjectNamenames it, holding exactly what was authored: the handler adds no label, no annotation and no default of its own. They are for the grants an application needs that are not those of one workload’s own ServiceAccount, which is what therbactrait (pkg/oam/builtin/traits/README.md) writes: a role several bindings share, a binding to a role that already exists, a grant to a user or a group, an aggregated ClusterRole. What the four have in common is stated here once; each kind’s entry below holds what is its own.- Authored: none of the four has a spec, so the properties are the
object’s own top-level fields, decoded strictly at every depth. The
object’s
kind,apiVersionandmetadataare launcher’s and are refused under any spelling. - Presence rules read by hand. Beyond the
+requiredmarkers of the linkedk8s.io/apimodule, these four kinds check presence rules read by hand from the API server’s validation at Kubernetes v1.37.1 (pkg/apis/rbac/validation/validation.goofk8s.io/kubernetes, which no module launcher links holds). No test derives them, and a later Kubernetes that changes one is not noticed by a dependency bump. - Required from the markers are the fields the API’s source marks
required and the Go type writes whether or not they were authored.
TestBuiltinMarkerKinds_RequiredMatchMarkersderives each kind’s list from the markers and fails on a dependency bump that changes it. The API marks no field of the four required that the type leaves out when unauthored; the same test fails if one appears.TestKindComponents_OmittedRequiredAndWrittenDefaultsholds the four in its table as well, from the same markers: to that, and to no default marked on a field the type writes unauthored. These checks are of presence: an authored empty value (verbs: [],name: "") builds and is the API server’s to refuse. - A rule (
rules[]of aroleor aclusterrole) must write itsverbs(rules[0].verbs: required …, from the markers). By hand: it must name at least one ofapiGroupsand at least one ofresources(rules[0].apiGroups: required …;""is the core group), so a rule that lost its resources and grants nothing is refused. Aclusterrolerule may instead namenonResourceURLs, and then names noapiGroups,resourcesorresourceNames(rules[0].nonResourceURLs: not allowed beside …). These two hand-read checks count what was decoded, so an authored emptyapiGroups: []orresources: []is refused too. - A binding (
rolebinding,clusterrolebinding) must writeroleRefwith itskindandname, and each subject’skindandname(roleRef: required …,subjects[0].name: required …, from the markers).subjectsmay be left out: the API server’s validation requires none, and the key is then not written. roleRef.apiGroupmay be left out. The object then carriesapiGroup: "", which the type always writes, and the API server fills the RBAC group before it validates. An authored group that is not the RBAC group is written as authored and is the API server’s to refuse. A User or Group subject may leave itsapiGroupout too: the key is then not written, and the API server fills the RBAC group the same way.- The names are the author’s literals.
roleRef.nameand a subject’snameandnamespaceare written as authored. Launcher points none of them at a component: aroleRef.namedoes not follow theobjectNameof aroleorclusterrolecomponent, and a ServiceAccount subject’s name does not follow aserviceaccountcomponent’s, so write the name the object takes. Whether the role or the subject exists is not checked. - Policy. No field of the four is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing. - Labels and annotations are the
labelsandannotationsproperties. - Beside the
rbactrait. A trait’s object and a component’s of one kind, name and namespace (or one cluster-scoped name) are refused: the trait resolves the name of each object it generates, and the transform reports a name collision that names both. - Not covered. Every rule on the form of a value is left to the API
server: that a
roleRefnames the RBAC group and a kind the binding may grant, the kind and API group of a subject, the form of its name, and whether thevaluesof a match expression of an aggregation selector fit itsoperator. Whether a verb, a resource or an API group exists, and the form of a non-resource URL, are checked by neither launcher nor the API server’s validation: a rule that names none that exists builds and passes that validation. The API server refuses a change to theroleRefof an existing binding. Launcher does not compare a build with the cluster, so a changed one builds here and is refused at apply. Launcher does not compare what a role grants with what its author, or whoever applies the object, may grant. - The object kind policy (
oam.ObjectKindPolicy, go-kure/launcher#922) is what restricts these kinds: it keeps any of the four, or the wholerbac.authorization.k8s.iogroup, out of a build, and a policy that does not allow cluster-scoped objects refuses the ClusterRole and the ClusterRoleBinding. It holds therbactrait’s objects too. What an allowed role grants stays unchecked here; the API server’s escalate and bind checks hold it to the identity that applies it.
- Authored: none of the four has a spec, so the properties are the
object’s own top-level fields, decoded strictly at every depth. The
object’s
role emits its Role in the build namespace; the handler declares its object as namespaced, so the object name is claimed in the object’s namespace. It is ungated: no capability and no environment-policy check restricts what a role grants. Its one property is
rules.nonResourceURLsare refused in a rule (rules[0].nonResourceURLs: not allowed in a Role …): a non-resource URL is not namespaced, and the API server refuses one in a Role. The API requires no rule, and the type always writes the list: a role that authors none carriesrules: nulland grants nothing, and an authored empty list is written as[].rolebinding emits its RoleBinding in the build namespace; the handler declares its object as namespaced, so the object name is claimed in the object’s namespace. It is ungated: no capability and no environment-policy check restricts what a role grants, nor to whom: a binding to any role the cluster holds,
cluster-adminincluded, builds. Its properties aresubjectsandroleRef, which names a Role of the binding’s namespace or a ClusterRole. A ServiceAccount subject may leave itsnamespaceout: the API server’s validation takes it, the key is not written, and the authorizer of that version reads the subject as one of the binding’s own namespace.clusterrole emits its ClusterRole with no namespace; the handler declares its object as cluster-scoped, so the object name is claimed cluster-wide. It is ungated: no capability and no environment-policy check restricts what a role grants. Its properties are
rulesandaggregationRule. By hand: an authoredaggregationRulemust hold at least one entry ofclusterRoleSelectors(aggregationRule.clusterRoleSelectors: required …); an empty selector ({}) is an entry. From the markers, a selector’s match expression must write itskeyandoperator. By hand again, an expression that writes both is refused for an operator that is none of the four and forvaluesthat do not go with the operator (aggregationRule.clusterRoleSelectors[0].matchExpressions[0].values: required with the operator In (at least one value)): the API server validates each selector as a label selector (see “A label selector’s match expressions”). An aggregated ClusterRole builds: it is emitted withrules: nullwhenrulesis not authored and withrules: []when authored so, and the control plane fills the rules.clusterrolebinding emits its ClusterRoleBinding with no namespace; the handler declares its object as cluster-scoped, so the object name is claimed cluster-wide. It is ungated: no capability and no environment-policy check restricts what a role grants, nor to whom: a binding of
cluster-adminto any subject builds. Its properties aresubjectsandroleRef, which names a ClusterRole. By hand: a ServiceAccount subject must write itsnamespace(subjects[0].namespace: required …), since the binding is in none.horizontalpodautoscaler (go-kure/launcher#790) is the kind-named projection of an
autoscaling/v2HorizontalPodAutoscaler. It emits that one object in the build namespace, named after the component unlessobjectNamenames it, holding exactly what was authored: the handler adds no label, no annotation and no default of its own. The handler declares its object as namespaced, so the object name is claimed in the object’s namespace.- Authored: the five fields of
HorizontalPodAutoscalerSpec, decoded strictly at every depth:scaleTargetRef,minReplicas,maxReplicas,metricsandbehavior. An unknown key is refused wherever it sits (a metric source, a scaling rule), a quantity written as a number is emitted in its canonical string form, and an authored0is kept (stabilizationWindowSeconds: 0). The type has no number or boolean that is omitted when zero, whichTestHorizontalPodAutoscalerSpec_NoOmittedZerosholds it to. - An empty
metricsorpolicieslist is refused. The type omitsmetricsand a scaling rule’spolicieswhen empty, and the API server then applies its default: 80% average CPU utilization formetrics, and its default policies forbehavior.scaleUpandbehavior.scaleDown. So an authored[]cannot be carried (metrics: [] cannot be carried by the Kubernetes API types (the field is omitted when empty, so the API server would apply its default …)). See Authored empty lists the API defaults below. - Required are the two top-level fields the API server refuses an
autoscaler without:
scaleTargetRef(scaleTargetRef: required …) andmaxReplicas(maxReplicas: required …; an authored0is refused the same way, since the type cannot tell it from an unset one). The config is exported, soGeneraterepeats both. Every other value rule is left to the API server: a negativemaxReplicas, thekindandnameinsidescaleTargetRef,minReplicasagainstmaxReplicas, which metric source goes with whichtype. The label selector of a metric (metrics[].object.metric.selector, and underpodsandexternal) is not held as other kinds hold theirs: the API server validates nothing of it, and a kind does not refuse what the API admits. - Policy.
maxReplicasis held to the environment policy’s replica maximum (MaxReplicas()), as thescalertrait holds its own and as a HorizontalPodAutoscaler a chart renders, apassthroughcomponent holds or amanifestssource yields is held:component "web": maxReplicas: replicas 4 exceeds enforced maximum 3. Nothing else is held, and no default is filled: the policy’sscalerMinReplicasandscalerMaxReplicasdefaults belong to thescalertrait. A nil policy checks nothing. - The target is the author’s, and is not checked.
scaleTargetRefnames an object bykind,nameandapiVersion; launcher points it at no component and does not look for the object in the document. To scale a workload component, name its object: the component name, or itsobjectName. The guards thescalertrait applies to its own workload do not see this kind: adeployment,webserviceorworkerwith a claim that is not ReadWriteMany refuses ascalertrait whosemaxReplicasis above 1, and builds beside ahorizontalpodautoscalercomponent that scales it further. That is the state of an autoscaler emitted throughpassthrough,manifestsor template delivery. - Beside the
scalertrait. The trait derives an autoscaler for the workload it is attached to, from a target CPU or memory utilization. This kind is the authored object, for what the trait does not express: other metric sources, scaling behavior, a target launcher does not own. The two name their objects apart (<component>-hpafor the trait), and two that are given one name in one namespace are refused as a name collision. - Labels and annotations are the
labelsandannotationsproperties. - Not covered. Whether the target exists or can be scaled. The object’s status is the controller’s and is not written.
- Authored: the five fields of
servicemonitor, podmonitor, prometheus-probe, prometheusrule (go-kure/launcher#790) are the kind-named projections of four objects of the Prometheus operator’s
monitoring.coreos.com/v1API: a ServiceMonitor, a PodMonitor, a Probe and a PrometheusRule. Each is built onpolicyFreeKindand emits that one object in the build namespace, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default of its own. Each declares its object as namespaced, so the object name is claimed in the object’s namespace. The Probe’s type name carries a prefix because a probe, in this package, is a container’s.No capability is required, and none gates these kinds. Launcher does not ask whether the cluster serves
monitoring.coreos.com/v1: where the operator’s CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component may author these. A policy can keep the kinds out of a build through the object kind policy (oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the top-level json fields of the spec type, decoded strictly at every depth: an unknown key is refused wherever it sits (an endpoint, a relabeling rule, a rule of a group).
servicemonitor(ServiceMonitorSpec):endpoints,selector,namespaceSelector,selectorMechanism,jobLabel,targetLabels,podTargetLabels,attachMetadata,bodySizeLimit,serviceDiscoveryRole, and the scrape settings below.podmonitor(PodMonitorSpec):podMetricsEndpoints,selector,namespaceSelector,selectorMechanism,jobLabel,podTargetLabels,attachMetadata,bodySizeLimit, and the scrape settings below.prometheus-probe(ProbeSpec):prober,targets,module,jobName,interval,scrapeTimeout,metricRelabelings,params, the HTTP client settings the type embeds (authorization,basicAuth,oauth2,bearerTokenSecret,followRedirects,enableHttp2,tlsConfig), and the scrape settings below.prometheusrule(PrometheusRuleSpec):groups, each with itsname,rulesand evaluation settings. A rule’sexpris a string or a number and is emitted as the one authored.- The scrape settings the three scrape kinds share:
sampleLimit,targetLimit,labelLimit,labelNameLengthLimit,labelValueLengthLimit,keepDroppedTargets,scrapeProtocols,fallbackScrapeProtocol,scrapeClass,scrapeNativeHistograms,scrapeClassicHistograms,nativeHistogramBucketLimit,nativeHistogramMinBucketFactorandconvertClassicHistogramsToNHCB. - An authored
0orfalseis kept where the API tells it from an unset field (sampleLimit: 0,filterRunning: false, a group’slimit: 0), and left out where the type omits a zero that means the same (honorLabels: false). A quantity written as a number is emitted in its canonical string form (nativeHistogramMinBucketFactor: 1.1as1100m). The API’s two defaults are strings (a prober’spath,/probe, and a relabeling rule’saction,replace): the type omits an empty string there, so an authored""would be defaulted, and it is refused (prober.path: "" cannot be carried by the Prometheus operator API types (…)).TestMonitoringKinds_DefaultedZerosholds each kind’s list of such fields to the numbers, booleans and strings that are omitted when zero and defaulted to something else, from the default markers of the linked module’s source.
Required is a field the API requires that the Go type writes whether or not it was authored, so that the object would not show the omission. It is the rule every kind follows, not a wider one: sent as it was authored, the document is one the API server refuses, and only the Go type’s zero value hides that. Each must be authored (
selector: required (…),endpoints[1].oauth2.tokenUrl: required (…)):- a
servicemonitor’sendpointsandselector, and apodmonitor’sselector. No default selector is filled: unauthored, the type would writeselector: {}, which selects every Service or pod of the selected namespaces.selector: {}andendpoints: []are authored values and build. - the
clientId,clientSecretandtokenUrlof anoauth2, on an endpoint of either monitor and on a Probe; - a rule group’s
nameand a rule’sexpr(unauthored, the type would writeexpr: 0); - the
keyandoperatorof a match expression, in a monitor’sselectorand in a Probe’stargets.ingress.selector(selector.matchExpressions[0].operator: required (…)). The selector is a Kubernetes type, which carries no+requiredmarker: the same test derives the two fields from that type’s source by the schema generators' rule, that a field with no optional marker whose json tag keeps it when empty is required. Presence only: the operator’s value and itsvaluesare left to the API server (see “A label selector’s match expressions”).
A required field the type leaves out when it is empty is refused too: the
nameof a Probe parameter (params[1].name: required (…)), unauthored or empty. The object would show the omission and the API server refuse it.TestKindComponents_OmittedRequiredAndWrittenDefaultsderives these fields for every kind in its table, from the CRD the linked module ships or, where it ships none, from the markers of its source. It holds each to a refusal, shown on a document, or to a stated reason. The same test derives the fields the API defaults and the type writes unauthored, where the default would never apply, and holds each to one of three answers: refused, filled by the kind with the API’s default, or harmless with the reason.The table holds the kind components that decode their properties into an upstream type, but for two groups. The
httproutekind is not in it: the experimental-channel CRD requires theprotocolof anexternalAuthfilter, of a rule and of a backend reference, the type leaves an empty one out, and the kind does not refuse it. The Flux kinds (helmrelease,fluxcd-kustomization,helmrepository,ocirepository,gitrepository,bucket,helmchart) are not in it either: no row is written for them.A Probe needs
prober.urlhere, because the object always carries a prober. This is a limit of the Go type, not a rule of the API: the API does not requireprober, but the type writes one whether or not it was authored, and the API server refuses a prober without aurl. A Probe without a prober cannot be emitted. An emptyurlis refused as an unauthored one is.TestMonitoringKinds_RequiredMatchMarkersholds these lists to the fields the source marks required and the type writes unauthored, the linked module’s and that of a match expression’s type, so a dependency bump that adds, drops or moves one fails there. Not refused:- another required field of a Kubernetes type these specs embed: the
keyof a Secret or ConfigMap key reference. An omitted one is emitted empty; - an authored empty string in a required field (a group’s
name: ""),prober.urland a parameter’snameexcepted. It is a value, and the API server’s to refuse; - every other value rule of the API (formats, enumerations, lengths, that a rule is a recording or an alerting one), and the operator’s own checks of an object the API server has admitted: that a Probe has targets, that a scrape timeout is no longer than its interval, that an endpoint or a Probe authenticates one way. Whether a rule’s expression is valid PromQL is not checked either.
What the type writes unauthored. A
servicemonitororpodmonitorcarriesnamespaceSelector: {}, which selects the object’s own namespace. Apodmonitorwithout endpoints carriespodMetricsEndpoints: null, which the API server drops as it drops every null of a field that is not nullable. Aprometheus-probecarriestargets: {}, and the operator rejects a Probe with no target. AtlsConfigcarriesca: {}andcert: {}where they were not authored.The selector, the prober and the targets are the author’s. Launcher points none at a component and looks for none in the document. To scrape a workload component’s pods, select their
applabel (matchLabels: {app: <component>}, valued as “Theapplabel” below describes); to scrape aservicecomponent’s Service, select a label its object carries (the component label, see “Component label and ownership” in the OAM model). A Secret or ConfigMap an object refers to is read by the operator in the object’s namespace.Selected by a Prometheus through the object’s labels. A Prometheus picks these objects up through its label selectors (
serviceMonitorSelector,podMonitorSelector,probeSelector,ruleSelector). The handler writes no label, so the object carries the component label, whose value is the component’s, and whatlabelsauthors: for a Prometheus that selects on a fixed label (release: <name>, say), write that label there.Policy. No field of these specs is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing, and each builds the same under every policy and under none.- Hosts are not checked. A host these objects name is one Prometheus
reaches, not an artifact source, and none is held to the policy’s allowed
registries:
prober.url, aproxyUrl(of an endpoint, a prober or anoauth2), anoauth2’stokenUrl, and the static targets of a Probe. - No field holds a literal secret by design, and none is checked. A
credential is a reference to a key of a Secret:
authorization.credentials,basicAuth,bearerTokenSecret, anoauth2’sclientSecret, atlsConfig’skeySecret, the values ofproxyConnectHeader. Free text that could hold one is written to the object as authored, under a policy that forbids explicit secrets too:params, anoauth2’sendpointParams, and aproxyUrlwith credentials in it. - File paths are not checked. A
servicemonitorendpoint may name a file in the Prometheus container (bearerTokenFile, andcaFile,certFileandkeyFileundertlsConfig).
Not covered. Whether what is selected or referred to exists (a Service, a pod, an Ingress, a named port, a Secret key), and whether a Prometheus of the cluster selects the object. The object’s status is the operator’s and is not written.
issuer, clusterissuer, certificate (go-kure/launcher#790) are the kind-named projections of three objects of cert-manager’s
cert-manager.io/v1API: an Issuer, a ClusterIssuer and a Certificate. Each is built onpolicyHeldKind(policyFreeKindwith a policy check, see the storageclass entry) and emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default. Anissuerand acertificateare emitted in the build namespace and declare their object as namespaced. Aclusterissueris emitted with no namespace and declares its object as cluster-scoped, so its name is claimed in no namespace. An Issuer and a ClusterIssuer share one spec type (IssuerSpec), and what is said of an issuer below holds for both kinds.No capability is required, and none gates these kinds. Launcher does not ask whether the cluster serves
cert-manager.io/v1: where cert-manager’s CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component may author these, a ClusterIssuer included. A policy can keep the kinds out of a build through the object kind policy (oam.ObjectKindPolicy, go-kure/launcher#922). Thecertificatetrait still requires its capability; the kind of the same name does not read it.Authored. The properties are the top-level json fields of the spec type, decoded strictly at every depth: an unknown key is refused wherever it sits (a solver, a DNS provider’s settings, a keystore).
issuerandclusterissuer(IssuerSpec): the five issuer types,acme,ca,vault,selfSignedandvenafi. An issuer type authored empty is in the object as written (selfSigned: {}is a complete self-signed issuer); one left out is not.certificate(CertificateSpec):secretName,issuerRef, the subject (commonName,subject,literalSubject), the subject alternative names (dnsNames,ipAddresses,uris,emailAddresses,otherNames), the lifetime (duration,renewBefore,renewBeforePercentage,renewal), the key and its use (privateKey,signatureAlgorithm,usages,encodeUsagesInRequest,isCA,nameConstraints), and what is written to the Secret (secretTemplate,keystores,additionalOutputFormats), withrevisionHistoryLimit.- A duration is carried in Go’s spelling.
duration,renewBeforeand the other durations of these types are authored as a Go duration string and nothing else, and the object carries the same duration as Go writes it:2160his emitted as2160h0m0s. - An authored
falseor0is kept where the API tells it from an unset field (encodeUsagesInRequest: false, a keystore’screate: false, a solver pod’srunAsUser: 0), and left out where the type omits a zero that means the same (isCA: false).TestCertManagerKinds_NoDefaultedZerosholds the types to having no number, boolean or string that is omitted when zero and that the CRD defaults to something else. A default cert-manager applies when it reads the object (a key size, a rotation policy) reads the same type, in which an authored0and none are one value.
Required is a field the API requires that the Go type writes whether or not it was authored, so that the object would not show the omission: the rule every kind follows (see the Prometheus operator’s kinds). Each must be authored (
secretName: required (…),acme.solvers[1].dns01.webhook.groupName: required (…)); an authored empty value is a value, and the API server’s to refuse.- A
certificate:secretName, andissuerRefwith itsname. No default issuer is filled. Of what is authored below them: an additional output format’stype, and a keystore’screate. - An issuer has no required top-level field. Of an issuer type that is
authored:
acme.serverandacme.privateKeySecretRef;ca.secretName;vault.server,vault.pathandvault.auth, and the role or path of the authentication method that is authored;venafi.zone, and the URL, credentials or token of the platform that is authored; a DNS01 provider’s own required settings (an ACME-DNShost, an RFC 2136nameserver, a webhook solver’sgroupNameandsolverName). - Of a reference to a Secret or a ServiceAccount that is authored, on either
kind: its
name. Unauthored, the type would writename: "". - Of a renewal window of a
certificatethat is authored (renewal.windows[]): itscronand itswindowDuration(renewal.windows[0].cron: required (…)). The type leaves either out when it is empty, so the object would show the omission and the API server refuse it; an emptycronis refused as an unauthored one is.TestKindComponents_OmittedRequiredAndWrittenDefaultsderives the two from the CRD and shows the refusals. - Of an ACME HTTP01 solver, on an issuer and a clusterissuer, three fields
of the Kubernetes and Gateway API types it embeds: the
nodeSelectorTermsof a required node affinity of the solver pod, underhttp01.ingressand underhttp01.gatewayHTTPRoute(acme.solvers[0].http01.ingress.podTemplate.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms: required (…)), and thenameof a parent reference (acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].name: required (…)). Unauthored, the type writes the terms asnull, which the API server drops before it validates, so the required field is then missing; and the name as"", which is below the CRD’s minimum length of 1.TestKindComponents_NullRequiredshows each refusal by running the CRD’s schema validator on the object the type would encode. An authored empty name (name: "") is not refused here: it is a value, and the API server’s to refuse for that minimum length. An authored empty list of terms (nodeSelectorTerms: []) is not refused either: it is written as one, and the CRDs accept it. - Of a match expression that is authored in a label selector of an HTTP01
solver’s pod affinity (
acme.solvers[].http01.ingress.podTemplate.spec.affinity, and undergatewayHTTPRoute), on an issuer and a clusterissuer: itskeyandoperator. Presence only: the operator’s value and itsvaluesare left to the API server (see “A label selector’s match expressions”). - A required field under a parent the author left out is not asked for: the list follows what was authored.
TestCertManagerKinds_RequiredMatchCRDholds the lists (96 paths for an issuer, 8 for a certificate) to the CRDs the linked module ships, which are the ones cert-manager’s chart installs: every field of cert-manager’s own types that a CRD requires and the type writes unauthored is listed, with the fields of an embedded type above (the three, and thekeyandoperatorof an expression at each of the 16 selectors), and nothing else is. A dependency bump that adds, drops or moves one fails there.TestLabelSelectorKinds_CoverEverySelectorshows, at each of those selectors, what the API server answers for an expression without itskeyor itsoperator. Not refused:- every other required field of a Kubernetes or Gateway API type these
specs embed (the
keyandoperatorof a node selector requirement, thetopologyKeyof an affinity term): the type writes it empty, and the CRDs accept it so; - every other value rule of the CRDs (enumerations, lengths, minima, and
the one rule the CRDs write as an expression: that a
venafiissuer names exactly one oftpp,cloudandngts); - the rules of cert-manager’s validating webhook, which refuses more
than the CRDs do: an issuer that configures no issuer type, or more than
one; a keystore with a
passwordbeside apasswordSecretRefthat names a Secret, or with neither (the empty reference the type writes beside an authoredpasswordis not one); a certificate that names no subject and no alternative name;subjectorcommonNamebesideliteralSubject. Launcher repeats none of them, so anissuerwith no property builds, and is refused at apply where the webhook runs.
What the type writes unauthored. An empty reference,
{name: ""}, where the type holds one by value and the API does not require it: a keystore’spasswordSecretRef(so a keystore that authorspasswordcarries both fields),vault.auth.kubernetes.secretRef, an RFC 2136 solver’stsigSecretSecretRefand a Route 53 solver’ssecretAccessKeySecretRef. An HTTP01 solver’spodTemplatecarriesmetadata: {}andspec: {}, and itsingressTemplatemetadata: {}. The CRDs accept each, and the same test holds every such field to its schema: none is refused empty.Policy.
- An issuer’s ACME HTTP01 solver pod is held to the cpu and memory
maxima. cert-manager starts a pod to answer an HTTP01 challenge, and a
solver’s
podTemplate.spec.resourcessizes it. Its limits and requests are held to the environment policy’s cpu and memory maxima, as a container’s are, on both ways a solver answers (http01.ingress,http01.gatewayHTTPRoute) and on every solver:component "web": acme.solvers[0].http01.ingress.podTemplate.spec.resources: cpu limit "4" exceeds enforced maximum "2". Nothing else is held and no default is filled: the policy’s default requests and limits are a workload’s. A request above its limit is not refused, since cert-manager lays the authored block over its controller’s own defaults key by key, and the pair that reaches the pod is not the authored one. A solver pod template has no other field thepodkind’s refusals speak to: it names no image (the solver’s image is a flag of the cert-manager controller), no container security context, no volume and no host namespace. Its pod-levelsecurityContext,serviceAccountName,priorityClassNameandimagePullSecretsare the author’s and are held to nothing, as are itsnodeSelector,affinityandtolerations, as they are on a pod kind: the one field of a pod-level security context the policy holds there,windowsOptions.hostProcess, is not a field of this one. - A certificate’s keystore password in the object is refused under a
policy that forbids explicit secrets.
keystores.jks.passwordandkeystores.pkcs12.passwordhold the password itself, and the object is in the build’s output:component "web": keystores.jks.password: holds the keystore password in the object, and the environment policy forbids explicit secrets; name the key of a Secret created out of band in keystores.jks.passwordSecretRef instead. The message quotes nothing of the value, and an empty password is refused as any other is. A policy that allows explicit secrets, one that does not answer the question and no policy build it. No other field of a Certificate holds a secret. - No field of an issuer holds a literal secret by design, and none is
checked. A credential is a reference to a Secret: an ACME account key,
a DNS provider’s token, a Vault token or App Role secret, a platform’s
API credentials. Free JSON that could hold one is written to the object
as authored, under a policy that forbids explicit secrets too: a webhook
solver’s
config. - Hosts are not checked. A host these objects name is one cert-manager
reaches, not an artifact source, and none is held to the policy’s allowed
registries:
acme.server,vault.server, avenafiplatform’surland token endpoint, an RFC 2136nameserver, an ACME-DNShost, an AkamaiserviceConsumerDomain, and the CRL distribution points, OCSP servers and issuing certificate URLs an issuer writes into what it signs. - A nil policy checks nothing.
Labels and annotations are the
labelsandannotationsproperties. A solver’spodTemplate.metadatais not these: it becomes the metadata of the pod cert-manager starts to answer an HTTP01 challenge. It is held as a pod’s is, with or without a policy: on both ways a solver answers and on every solver, the component label’s key holds the component’s value there or the build is refused (Issuer "web": spec.acme.solvers[0].http01.ingress.podTemplate.metadata.labels[…]: "db" is not the component label of component "web"), and a key the consumer reserved is refused (solver pod template label "…" (spec.acme.solvers[0].http01.ingress.podTemplate.metadata.labels) may not be set); see “Component label and ownership” and “Reserved metadata keys” in the OAM model. Launcher writes nothing there. A solver’singressTemplate.metadataand thelabelsof its HTTPRoutes reach an Ingress and HTTPRoutes, and are not read.The issuer of a certificate is the author’s.
issuerRefnames an issuer byname,kindandgroup; launcher points it at no component and does not look for the issuer in the document. To have anissuerorclusterissuercomponent sign acertificatecomponent, name its object: the component name, or itsobjectName. cert-manager reads anissuerRefwithout akindas an Issuer of the Certificate’s namespace. The SecretsecretNamenames is cert-manager’s to create, and the component emits none; a Secret an issuer refers to is read in the Issuer’s namespace, and for a ClusterIssuer in the namespace cert-manager is configured with.Beside the
certificatetrait. The trait derives a Certificate for the workload it is attached to, from a few properties and the issuer the cluster’scertificatecapability names, and names it after itssecretName. This kind is the authored object, for what the trait does not express: the whole spec, an issuer the author chooses, a Certificate that belongs to no workload. A component type and a trait type are two lists, so the one name is not ambiguous in a document. The two objects are one kind: a trait’s and a component’s given one name in one namespace are refused (generated-object collision: Certificate.cert-manager.io "default/app-tls" is generated by both …).Not covered. Whether what is referred to exists (an issuer, a Secret and its key, a ServiceAccount, an ingress class, a Gateway), and whether cert-manager can reach what an issuer names. The object’s status is cert-manager’s and is not written.
cilium-bgpadvertisement, cilium-bgpclusterconfig, cilium-bgpnodeconfigoverride, cilium-bgppeerconfig (go-kure/launcher#790) are the kind-named projections of four objects of Cilium’s BGP control plane, in its
cilium.io/v2API: a CiliumBGPAdvertisement, a CiliumBGPClusterConfig, a CiliumBGPNodeConfigOverride and a CiliumBGPPeerConfig. Each is built onpolicyFreeKindand emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default. All four are cluster-scoped: the object carries no namespace, whatever namespace the application is built for, and its name is claimed cluster-wide. The type names carry thecilium-prefix: other products have a BGPAdvertisement and a BGP peer of their own. The fifth object of that API, the CiliumBGPNodeConfig, is generated by the Cilium operator and has no component.No capability is required, and none gates these kinds. Launcher does not ask whether the cluster runs Cilium or has its BGP control plane enabled: where the CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component may author these, and with them what the cluster’s nodes announce to its routers. A policy can keep the kinds out of a build through the object kind policy (
oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the top-level json fields of the spec type, decoded strictly at every depth: an unknown key is refused wherever it sits (an advertisement, an instance, a peer, a family).
cilium-bgpadvertisement(CiliumBGPAdvertisementSpec):advertisements, each with itsadvertisementType(PodCIDR,CiliumPodIPPool,ServiceorInterface),service(addresses,aggregationLengthIPv4,aggregationLengthIPv6),interface(name),selectorandattributes(communities,localPreference).cilium-bgpclusterconfig(CiliumBGPClusterConfigSpec):nodeSelectorandbgpInstances, each with itsname,localASN,localPortandpeers; a peer has aname, apeerAddressor anautoDiscovery, apeerASNand apeerConfigRef.cilium-bgpnodeconfigoverride(CiliumBGPNodeConfigOverrideSpec):bgpInstances, each with itsname,routerID,localPort,localASNandpeers; a peer has aname, alocalAddressand alocalPort.cilium-bgppeerconfig(CiliumBGPPeerConfigSpec):transport(peerPort,sourceInterface),timers(connectRetryTimeSeconds,holdTimeSeconds,keepAliveTimeSeconds),authSecretRef,gracefulRestart(enabled,restartTimeSeconds),ebgpMultihopandfamilies, each with itsafi, itssafiand theadvertisementsit sends. A peer configuration that authors nothing builds, and carries nothing.- An authored
0orfalseis kept: every number these specs hold is a pointer in the Go type (peerASN: 0, which accepts any ASN the peer opens with;aggregationLengthIPv4: 0), andgracefulRestart.enabledis always written. No default is filled. The API’s defaults (a peer’speerASN, the threetimers,transport.peerPort,ebgpMultihop,gracefulRestart.restartTimeSeconds) are the API server’s to fill where the field was not authored;TestCiliumBGPKinds_DefaultsSitOnPointersholds each to a pointer field, from the CRDs of the linked module.
Required follows the rule of the Prometheus operator’s kinds: a field the API requires that the Go type writes whether or not it was authored. Each must be authored (
bgpInstances[0].peers[1].name: required (…)):- a
cilium-bgpadvertisement’sadvertisementsand an entry’sadvertisementType; - a
cilium-bgpclusterconfig’sbgpInstances, an instance’sname, a peer’sname, anautoDiscovery’smode, theaddressFamilyof itsdefaultGateway, and thenameof apeerConfigRef; - a
cilium-bgpnodeconfigoverride’sbgpInstances, an instance’snameand a peer’sname; - a
cilium-bgppeerconfigfamily’safiandsafi, and theenabledof agracefulRestart, which has no default: unauthored, the type would writeenabled: false; - the
keyand theoperatorof a match expression, in every selector of the four:nodeSelector, an advertisement’sselector, a family’sadvertisements. The selector is Cilium’s own type and its fields are in the CRD, so these are derived with the rest; on the Prometheus operator’s kinds, whose selector is the Kubernetes one, they are not.
TestCiliumBGPKinds_RequiredMatchCRDholds these lists to the fields the linked module’sv2CRD requires and the type writes unauthored, so a dependency bump that adds, drops or moves one fails there.Two required fields the type leaves out when they are empty are refused too, on an advertisement: the
nameof aninterface(advertisements[0].interface.name: required (…)) and theaddressesof aservice, an authoredaddresses: []included (advertisements[0].service.addresses: required (…)). The object would show the omission (service: {}) and the API server refuse it. The same test holds the two to the CRD, andTestKindComponents_OmittedRequiredAndWrittenDefaultsshows the refusals.The CRDs’ expression rules. A rule is checked where it is one comparison of fields, as authored or as the CRD defaults them, and the refusal can name what it compared; every rule the four CRDs declare is classified in
TestCiliumBGPKinds_ExpressionRules, which fails on one that is added or reworded. The same test holds each checked rule to the API server’s own expression validator, run over the linked CRD after the defaults the API server fills: it refuses the properties that break the rule, by that rule alone, and accepts the ones next to them, and the kind answers the same on both. Checked:- the five rules on an advertisement, each the entry’s
advertisementTypeagainst the presence of one sibling:serviceis required withServiceand refused with another type,interfaceis required withInterfaceand refused with another type, andselectoris refused withPodCIDR(advertisements[1].service: not allowed with advertisementType "CiliumPodIPPool", only with "Service"). An authored{}is a present field and an authorednullan absent one, as the API server reads them; - a peer configuration’s
timers.keepAliveTimeSecondsno larger than itstimers.holdTimeSeconds(timers.keepAliveTimeSeconds: 90 is larger than timers.holdTimeSeconds (30)). The API server evaluates the rule after it has filled the defaults, so with one of the two authored it compares that one with the default of the other, and so does the kind: a keepalive of 30 and a hold time of 90, the linked CRD’s, which the kind states and the test holds to that CRD.keepAliveTimeSeconds: 100alone is refused (timers.keepAliveTimeSeconds: 100 is larger than the hold time the API fills where timers.holdTimeSeconds is not set (90)), and so isholdTimeSeconds: 20alone (timers.holdTimeSeconds: 20 is smaller than the keepalive time the API fills where timers.keepAliveTimeSeconds is not set (30)). A cluster whose installed CRD fills other defaults than the linked one is not known here.
Not checked, and the API server’s to refuse:
- an authored empty value in a required field (
bgpInstances: [], an instance’sname: ""). It is a value. The two fields above are the exception: the type leaves an empty one out; - every other value rule of the API: the enumerations
(
advertisementType,afi,safi, a service address type, a well-known community), the ranges of an ASN, a port, a timer, an aggregation length andebgpMultihop, the forms of an address, a router ID and a community, the number of instances (one to sixteen in a cluster configuration, at least one in an override), an instance name unique in its list and a peer name unique in its instance; - what Cilium itself decides of an object the API server has admitted.
Labels and annotations are the
labelsandannotationsproperties. A peer configuration selects advertisements by label. A family’sadvertisementsis a label query over CiliumBGPAdvertisement objects: write the labels it names under thelabelsof thecilium-bgpadvertisement. The object also carries the component label, whose value is the component’s (see “Component label and ownership” in the OAM model).nodeSelectorand an advertisement’sselectorquery nodes, pools and Services, whose labels are their owners'.An override takes effect by its name. A CiliumBGPNodeConfigOverride overrides the CiliumBGPNodeConfig of the same name, the per-node object the Cilium operator generates; Cilium’s API says the two names must match exactly. Name the component so, or set
objectName. Launcher does not know the cluster’s objects and checks neither.Policy. No field of these specs is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing, and each builds the same under every policy and under none.- Hosts are not checked. An address these objects name is a BGP peer’s
or the node’s own, not an artifact source, and none is held to the
policy’s allowed registries: a peer’s
peerAddressin acilium-bgpclusterconfig, and a peer’slocalAddressand an instance’srouterIDin acilium-bgpnodeconfigoverride. - No field holds a literal secret, and none is checked.
authSecretRefis the name of a Secret Cilium fetches the session’s TCP authentication password from. The field holds a name and no namespace: where Cilium looks the Secret up is its own configuration. Launcher does not emit that Secret for it and does not check that one exists.
Not covered. The served
v2alpha1version of these objects: the kinds emitv2. Whether what is selected or referred to exists (a node, a pool, a Service, an advertisement, a peer configuration, a Secret), and whether the cluster’s Cilium runs its BGP control plane. Acilium-bgpclusterconfig’s and acilium-bgppeerconfig’s status is the operator’s and is not written.cilium-cidrgroup, cilium-loadbalancerippool, cilium-egressgatewaypolicy, cilium-localredirectpolicy, cilium-nodeconfig (go-kure/launcher#790) are the kind-named projections of five more objects of Cilium’s
cilium.io/v2API: a CiliumCIDRGroup, a CiliumLoadBalancerIPPool, a CiliumEgressGatewayPolicy, a CiliumLocalRedirectPolicy and a CiliumNodeConfig. Each is built onpolicyFreeKindand emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default of its own. The first three are cluster-scoped: the object carries no namespace, whatever namespace the application is built for, and its name is claimed cluster-wide. The redirect policy and the node configuration are namespaced and are written in the build namespace.No capability is required, and none gates these kinds, as for the BGP kinds: where the CRDs are not installed, or the feature the object configures is not enabled in the cluster’s Cilium (egress gateway, local redirect policies, load balancer address management), the component builds. Whoever may author a component may author these, and with them the address a workload’s traffic leaves the cluster with, where a node sends traffic meant for an address or a Service, and the configuration of the Cilium agent on a node. A policy can keep the kinds out of a build through the object kind policy (
oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the top-level json fields of the spec type, decoded strictly at every depth: an unknown key is refused wherever it sits (a block, a selector, a gateway, a matcher, a port).
cilium-cidrgroup(CiliumCIDRGroupSpec):externalCIDRs.cilium-loadbalancerippool(CiliumLoadBalancerIPPoolSpec):serviceSelector,allowFirstLastIPs(YesorNo),blocks, each acidror astartwith an optionalstop, anddisabled.cilium-egressgatewaypolicy(CiliumEgressGatewayPolicySpec):selectors, each with anamespaceSelector, apodSelectorand anodeSelector;destinationCIDRs;excludedCIDRs;egressGatewayandegressGateways, a gateway being anodeSelector, aninterfaceand anegressIP.cilium-localredirectpolicy(CiliumLocalRedirectPolicySpec):redirectFrontend, anaddressMatcher(ip,toPorts) or aserviceMatcher(serviceName,namespace,toPorts);redirectBackend(localEndpointSelector,toPorts);skipRedirectFromBackend;description. A port is aport, aprotocoland aname.cilium-nodeconfig(CiliumNodeConfigSpec):defaults, a map of configuration keys to string values, andnodeSelector.- No default is filled, and an authored value that is the API’s default
is not written where the Go type cannot hold it. Three fields of these
specs are no pointers and are omitted when empty: a pool’s
disabled, a redirect policy’sskipRedirectFromBackendand an egress gateway policy’segressGateways. An authoreddisabled: false,skipRedirectFromBackend: falseoregressGateways: []is left out of the object, and the API server fills the same value back: each of the three has that empty value as its default in the CRD.TestCiliumPlainKinds_NoDefaultIsLostholds the defaults of the linked CRDs to that: one that is not the field’s empty value, on a field that is no pointer, fails there.
Required follows the rule of the BGP kinds: a field the API requires that the Go type writes whether or not it was authored. Each must be authored (
egressGateways[1].nodeSelector: required (…)):- a
cilium-cidrgroup’sexternalCIDRs; - a
cilium-egressgatewaypolicy’sselectors,destinationCIDRsandegressGateway, and thenodeSelectorofegressGatewayand of every entry ofegressGateways; - a
cilium-localredirectpolicy’sredirectFrontendandredirectBackend; anaddressMatcher’sipandtoPorts; aserviceMatcher’sserviceNameandnamespace; the backend’slocalEndpointSelectorandtoPorts; theportand theprotocolof every port, in each of the three lists; - a
cilium-nodeconfig’sdefaultsandnodeSelector; - the
keyand theoperatorof a match expression, in every selector of the five. Acilium-loadbalancerippoolrequires nothing else.
An authored empty value satisfies the rule:
externalCIDRs: []is a group that selects no peer,nodeSelector: {}every node anddefaults: {}no key.TestCiliumPlainKinds_RequiredMatchCRDholds these lists to the fields the linked module’sv2CRD requires and the type writes unauthored, so a dependency bump that adds, drops or moves one fails there.A redirect frontend takes exactly one matcher. The CRD’s schema of
redirectFrontendis a choice betweenaddressMatcherandserviceMatcher, and the API server refuses neither and both. It is one comparison of two authored fields, so the kind refuses it too (redirectFrontend: one of addressMatcher and serviceMatcher is required;redirectFrontend: addressMatcher and serviceMatcher are both set; the API takes exactly one).TestCiliumPlainKinds_RedirectFrontendTakesOneMatcherholds the check to the CRD, and fails where another schema of the five gains such a choice.The CRDs’ expression rules. The five CRDs declare five, and none is checked;
TestCiliumPlainKinds_ExpressionRuleslists each with why, and fails on one that is added or reworded.Not checked, and the API server’s to refuse:
- a gateway’s
egressIPbeing an IP address, the CRD’s rule onegressGateway.egressIPand on anegressGatewaysentry’s: the form of one field’s value, not a comparison of authored fields; - the three rules that refuse a change of a redirect policy’s
redirectFrontend,redirectBackendandskipRedirectFromBackendonce the object exists. Each compares the object with the stored one, and a build has none: such a change builds here and is refused at apply, until the object is deleted and created again; - a
cilium-nodeconfig’sdefaults: no key and no value is read. A key Cilium does not know builds; - an authored empty value in a required field (
selectors: [], aserviceName: ""). It is a value; - every other value rule of the API: the enumerations (
allowFirstLastIPs, a port’sprotocol), the forms of a CIDR, an address, a port and a port name, and the number ofegressGateways(at most 64); - what Cilium itself decides of an object the API server has admitted: a
serviceMatcherwhosenamespaceis not the policy’s own, which Cilium’s API says must match, and two pools whose blocks overlap, which the Cilium operator reports in the pool’s status.
Labels and annotations are the
labelsandannotationsproperties. A CIDR group is referred to by its name, or selected by its labels. A Cilium network policy’scidrGroupRefnames the group: name the component so, or setobjectName. ItscidrGroupSelectoris a label query over groups: write the labels it names under thelabelsof thecilium-cidrgroup. The object also carries the component label, whose value is the component’s (see “Component label and ownership” in the OAM model). Every other selector in the five specs queries objects launcher does not write the labels of here (Services, nodes, namespaces, pods) and is the author’s.Policy. No field of these specs is one an
oam.Policymethod speaks to, soApplyPolicyenforces nothing and fills nothing, and each builds the same under every policy and under none.- Hosts are not checked. No field names a host. An address or a CIDR
these objects hold is a network’s, not an artifact source, and none is
held to the policy’s allowed registries: a group’s
externalCIDRs, a pool’sblocks, an egress policy’sdestinationCIDRs,excludedCIDRsandegressIP, and a redirect frontend’sip. - No field holds a literal secret or refers to a Secret, and none is
checked. A
cilium-nodeconfig’sdefaultsis free text: a value written there is written into the object as authored.
Not covered. The served
v2alpha1version of the CIDR group, of the pool and of the node configuration: the kinds emitv2. Whether what is selected or referred to exists (a Service, a node, a namespace, a pod, an interface). A pool’s and a redirect policy’s status is the operator’s and is not written.cilium-clusterwidenetworkpolicy (go-kure/launcher#790) is the kind-named projection of a cluster-scoped cilium.io/v2 CiliumClusterwideNetworkPolicy. The object holds what a CiliumNetworkPolicy holds: one rule under
spec, a list of rules underspecs, or both, each a Ciliumapi.Rule. The component’s properties are those two fields, decoded as thecilium-networkpolicykind decodes them: strictly, under the null contract, with an unknown key inside a selector, anicmpsfield or a rule label refused by its path (see that entry).TestCoreKindSchemas_CoverSpecholds the two keys to the linkedCiliumClusterwideNetworkPolicytype. It emits one object, named after the component unlessobjectNamenames it, with no namespace, declared cluster-scoped, holding the authored rules and, as on every kind component, the labels and annotations written underlabelsandannotations. It differs from the namespaced kind in two things: its rules reach every namespace of the cluster, and a rule selects endpoints or nodes, byendpointSelectoror bynodeSelector.Refused at build: what the CRD’s schema refuses of the two fields, and nothing wider. Three rules of that schema, each one comparison of authored fields:
- no rule at all, neither
specnor an entry inspecs(the object’s expression rulehas(self.spec) || has(self.specs)). An emptyspecsis not emitted, so it holds none; beside aspecit is accepted.TestCiliumClusterwideNetworkPolicy_ObjectRuleholds the refusal to the API server’s own expression validator, which accepts an authoredspecs: []and would refuse the object written without it; - a rule that authors both
endpointSelectorandnodeSelector, or neither: the schema takes exactly one. None is filled in.endpointSelector: {}selects every endpoint of the cluster andnodeSelector: {}every node, and each is carried as written; a null one is an absent one; - a rule with no entry in any of
ingress,ingressDeny,egressandegressDeny. A deny list counts as much as an allow list; a null or empty list is not emitted and holds no entry.
A null entry of
specsis refused by its path.Required. Inside a rule the CRD requires 59 fields that the Cilium type writes whether or not they were authored, so that the object would not show the omission. Each is refused where its parent is authored, by its path (
spec.ingress[0].toPorts[0].terminatingTLS.secret: required (…)):- the
keyandoperatorof a match expression, in every selector of a rule (endpointSelector,nodeSelector,fromEndpoints,toEndpoints,fromNodes,toNodes, a CIDR entry’scidrGroupSelector, ak8sServiceSelector’sselector), under the deny lists too; - a
k8sServiceSelector’sselector, thekeyof a rule label and thetypeof anicmpsfield; - under an allow list only: an
authentication’smode; a port’slistenername, itsenvoyConfigand that configuration’sname; thesecretof a port’sterminatingTLSandoriginatingTLSand that Secret’sname; an HTTP header match’sname, and thenameof itssecretwhere one is authored.
Two more are optional to the API and required here: a listener’s
priorityand thekindof itsenvoyConfig. The type writes an unauthored one as0and as the empty string, and the API refuses both (a priority is 1 to 100, a kind isCiliumEnvoyConfigorCiliumClusterwideEnvoyConfig), so a listener that leaves one out cannot be emitted.TestCiliumClusterwideNetworkPolicy_RequiredMatchCRDholds the whole list to the CRD of the linked module and to the rule type, underspecand underspecs, so a dependency bump that adds, drops or moves one fails there. Anicmpsfield without itstypeand a rule label without itskeyare refused earlier, by Cilium’s own decoding, with the decode error (for thetype, one that names a panic and not the field’s position).Not checked. These are the API server’s to refuse, or Cilium’s:
- the choices the schema makes deeper in a rule: a CIDR entry (
toCIDRSet,fromCIDRSet) holds exactly one ofcidr,cidrGroupRefandcidrGroupSelector; a DNS entry (toFQDNs, a port’srules.dns) exactly one ofmatchNameandmatchPattern; a port’srulesexactly one ofhttpanddns.TestCiliumClusterwideNetworkPolicy_SchemaChoiceslists every choice and expression rule of the linked CRD as checked or left, and fails on one that is neither; - every value rule: enumerations, patterns, ranges, lengths and formats (an
authenticationmode, a port, a CIDR, an ICMP family), and an authored empty value in a required field; - what Cilium’s own reader refuses of an object the API server has admitted
(which peers may be combined, an entity’s name, a port’s range). The
cilium-networkpolicykind leaves these too; - whether what a rule refers to exists: a CIDR group, a Secret, an Envoy configuration, a Service.
What the object carries that was not written. An
icmpsfield’sfamilyis left out when it is not authored, and the API fillsIPv4, the one default the CRD declares in a rule. An authoredfamily: ""is left out as well and reads the same, to Cilium too. A rule label in its object form without asourcecarriessource: "", which the type writes and the API admits.Policy.
ApplyPolicyis a no-op and no capability is required, as for thecilium-networkpolicykind: the environment policy holds no rule for the object, and its capability lists gate trait types.- Hosts are not checked. A rule names hosts and networks: the names and
patterns of
toFQDNsand of a port’s DNS rules, an HTTP rule’shost, a port’sserverNames, and every CIDR. None is an artifact source, and none is held to the policy’s allowed registries. - No literal secret is checked. An HTTP header match’s
valueis written into the object as authored. A header match’ssecretand a TLS context’ssecretrefer to a Secret by name and namespace; the reference is written as authored, and the Secret is not looked for in the document.
Not covered. The object’s metadata beyond its name, its labels and its annotations (
objectName,labels,annotations), and itsstatus, which the Cilium agent writes.- no rule at all, neither
gatewayclass, gateway, listenerset, referencegrant, backendtlspolicy (go-kure/launcher#790) are the kind-named projections of the infrastructure objects of the Gateway API’s
gateway.networking.k8s.io/v1: a GatewayClass, a Gateway, a ListenerSet, a ReferenceGrant and a BackendTLSPolicy. Each is built onpolicyFreeKind(see the storageclass entry) and emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default. Agatewayclassis emitted with no namespace and declares its object as cluster-scoped, so its name is claimed in no namespace. The other four are emitted in the build namespace and declare their object as namespaced. The routes of the same API are their own kinds (httproute,grpcroute,tcproute,udproute,tlsroute).No capability is required, and none gates these kinds. Launcher does not ask whether the cluster serves
gateway.networking.k8s.io/v1: where the Gateway API’s CRDs are not installed the component builds, and the object is refused at apply. Whoever may author a component may author these: a GatewayClass, which is cluster-scoped, and a ReferenceGrant, which lets objects of another namespace refer to objects of the build namespace, included. A policy can keep the kinds out of a build through the object kind policy (oam.ObjectKindPolicy, go-kure/launcher#922). Thehttproutetrait may take the Gateway it attaches to from a capability (gatewayName,gatewayNamespace); thegatewaykind does not read it, and agatewaycomponent is not what that capability names unless the platform says so.Authored. The properties are the top-level json fields of the spec type, decoded strictly at every depth: an unknown key is refused wherever it sits (a listener, a certificate reference, a validation).
gatewayclass(GatewayClassSpec):controllerName,parametersRefanddescription.gateway(GatewaySpec):gatewayClassName,listeners,addresses,infrastructure,allowedListeners,tlsanddefaultScope.defaultScopeis an experimental-channel field: the Go type holds it and the standard channel’s Gateway CRD does not, so a cluster on that channel does not keep it. It is the only such field of these five specs at v1.6.3, andTestGatewayKinds_RequiredMatchCRDholds that.listenerset(ListenerSetSpec):parentRefandlisteners, whose entries take the fields of a Gateway’s listener.referencegrant(ReferenceGrantSpec):fromandto.backendtlspolicy(BackendTLSPolicySpec):targetRefs,validationandoptions.- No default is filled. The defaults are the CRDs’, and the API server
applies them to what the object leaves out: a listener’s
allowedRoutes(routes of the Gateway’s own namespace), a listener’stls.mode(Terminate), an address’stype(IPAddress), thegroupandkindof a certificate reference (a core Secret) and of a parent reference (a Gateway), thegroupof an allowed route kind (the Gateway API’s),allowedListeners.namespaces.from(None), a client certificate validation’smode(AllowValidOnly). - One number of these types is omitted when zero, a ListenerSet listener’s
port, and the CRD gives it no default: an authoredport: 0is left out as an unset one is, the API accepts neither, and the kind refuses both (below). Of the strings the types omit when empty, the CRDs default one: themodeof a Gateway’s frontend TLS validation (AllowValidOnly), so an authoredmode: ""would be left out and defaulted, and it is refused (tls.frontend.default.validation.mode: "" cannot be carried by the Gateway API types (…)).TestGatewayKinds_DefaultedZerosholds each kind’s list of such fields to the numbers, booleans and strings that are omitted when zero and that a CRD of either channel defaults to something else.
Required is a field the API requires that the Go type writes whether or not it was authored, so that the object would not show the omission: the rule every kind follows (see the Prometheus operator’s kinds). Each must be authored (
gatewayClassName: required (…),listeners[1].port: required (…)); an authored empty value is a value, and the API server’s to refuse.- A
gatewayclass:controllerName. - A
gateway:gatewayClassNameandlisteners, and of each listener itsname,portandprotocol. Of a Gateway-widetls.frontendthat is authored: itsdefault(default: {}asks for no client certificate), and of aperPortentry itsportandtls; of avalidationthat is authored, itscaCertificateRefs. - A
listenerset:parentRefwith itsname. - A
referencegrant:fromandto; of a source itsgroup,kindandnamespace, of a target itsgroupandkind. The core group is authored asgroup: "". - A
backendtlspolicy:validationwith itshostname; of a subject alternative name that is authored, itstype. - Of a reference that is authored, on every kind, what the type would write
empty: the
nameof a certificate reference (a listener’stls.certificateRefs, the Gateway’stls.backend.clientCertificateRef); thegroup,kindandnameof a parameters reference (parametersRef,infrastructure.parametersRef), of a CA certificate reference (caCertificateRefs) and of a policy target (targetRefs); thekindof an allowed route kind (allowedRoutes.kinds). - Of a match expression that is authored in a namespace
selector, on agatewayand alistenerset(a listener’sallowedRoutes.namespaces.selector, a Gateway’sallowedListeners.namespaces.selector): itskeyandoperator, which the CRDs of both channels require. Presence only: the operator’s value and itsvaluesare left to the API server (see “A label selector’s match expressions”). The selector is the one Kubernetes type these specs embed; the test below derives these entries with the others, andTestLabelSelectorKinds_CoverEverySelectorshows what the API server answers for them. - A required field under a parent the author left out is not asked for: the list follows what was authored.
A required field the type omits is refused by the kind itself, since no required list can name it: the type leaves it out where it is not authored and where it is authored empty alike, and the API server refuses the object either way. There are five at v1.6.3:
- a
listenersetwith nolistenersand abackendtlspolicywith notargetRefs, absent or empty (listeners: required (…)), as aservicecidrwith nocidrsis; - a ListenerSet listener without its
name, itsportor itsprotocol, absent or empty, aport: 0included (listeners[1].port: required (…)). A Gateway’s listener must author the same three, by the list above: the two listener types carry different json tags, and the answer is the same on both kinds.
TestGatewayKinds_RequiredMatchCRDderives that set from the CRDs as it derives the lists, at every depth of the five specs, and holds each member to one of two answers: the kind’s refusal, which it runs, or a stated reason the decoded value cannot show the omission. None has the second answer.TestGatewayKinds_RefusedOmissionsholds each refused field to being required in both channels.TestGatewayKinds_RequiredMatchCRDholds the lists (4 paths for a gatewayclass, 26 for a gateway, 6 for a listenerset, 7 for a referencegrant, 9 for a backendtlspolicy) to the CRDs the linked module ships. The module ships two sets, one per channel of the API. The experimental one holds every field the Go types do and is the one each path is read in; the standard one must require the same wherever it holds the property. Every field of the Gateway API’s own types that the CRDs require and the type writes unauthored is listed, with thekeyandoperatorof a namespace selector’s expressions, and nothing else is; no field is written empty unauthored beside those. A dependency bump that adds, drops or moves one fails there. Not refused:- a list the API wants at least one item of that is authored empty, where
the type writes it (
listeners: []on agateway,from: []): an authored empty value is a value; - every other value rule of the CRDs: enumerations, lengths, patterns,
minima and item limits, and the rules the CRDs write as expressions (a
listener’s
tlsandhostnameagainst itsprotocol, the uniqueness of a listener’s name and of its port, protocol and hostname, of an address and of aperPortport, one ofcaCertificateRefsandwellKnownCACertificates, a subject alternative name’s field against itstype, a target’ssectionNameamong several targets, a GatewayClass’scontrollerNamenot changing); - what a Gateway API controller refuses when it reads the object, which shows in the object’s status, not at creation.
Policy. No dimension of the environment policy reaches these objects: they run no pod, hold no image, request no storage and have no replica count. A nil policy and a strict one build the same object.
- The pods of a Gateway are not sized here. A controller may start a
proxy for a Gateway; the Gateway object holds no pod template and no
resources, so the policy’s maxima and defaults have nothing to hold.
infrastructure.labelsandinfrastructure.annotationsare written as authored, and what a controller reads into them is its own; they are held to the component label and the reserved keys with or without a policy (see “Labels and annotations” below). - No field holds a literal secret, and none is checked. A certificate is
a reference: to a Secret on a listener and for the Gateway’s client
certificate, to a ConfigMap or another object for CA certificates. The
free maps a controller defines, a listener’s
tls.optionsand a BackendTLSPolicy’soptions, are written to the object as authored, under a policy that forbids explicit secrets too. - Hosts are not checked. A host these objects name is one a Gateway
serves or a backend is checked against, not an artifact source, and none
is held to the policy’s allowed registries: a listener’s
hostname, an address’svalue, a BackendTLSPolicy’svalidation.hostnameand thehostnameanduriof its subject alternative names.
References are the author’s. A Gateway names its class (
gatewayClassName), a ListenerSet its Gateway (parentRef), a BackendTLSPolicy its backends (targetRefs), and each of them Secrets or ConfigMaps; launcher points none at a component and does not look for the target in the document. To refer to another component’s object, name it: the component name, or itsobjectName. A ListenerSet attaches only where its Gateway’sallowedListenerslets its namespace, and a reference across namespaces works only where a ReferenceGrant of the target’s namespace allows it; launcher checks neither. Areferencegrantallows references into the namespace it is emitted in, so it belongs to the application of the objects referred to.Labels and annotations are the
labelsandannotationsproperties. A Gateway’sinfrastructure.labelsandinfrastructure.annotationsare not these: they are what the controller puts on the resources it creates for the Gateway, which may be pods. They are held as a pod’s are: the component label’s key holds the component’s value there or the build is refused, and a key the consumer reserved is refused (spec.infrastructure label "…" may not be set); see “Component label and ownership” and “Reserved metadata keys” in the OAM model. Launcher writes nothing there.Not covered. Whether what is referred to exists (a class, a Gateway, a Secret, a ConfigMap, a Service and its port), and whether a controller of the cluster implements the class. The object’s status is the controller’s and is not written.
tcproute, udproute, tlsroute (go-kure/launcher#790) are the kind-named projections of the routes of the Gateway API’s
gateway.networking.k8s.io/v1that carry no HTTP: a TCPRoute, a UDPRoute and a TLSRoute. Each is built onpolicyFreeKind(above), as the kinds of the API’s infrastructure objects are, and emits that one object in the build namespace, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default. The object is written atv1, the version the CRDs of both channels store; the older versions (v1alpha2, andv1alpha3of a TLSRoute) are served by the experimental channel’s CRDs only, as deprecated.No capability is required, and none gates these kinds, on the terms of the infrastructure kinds above: launcher does not ask whether the cluster serves the API, and where the CRD is not installed the component builds and the object is refused at apply. Whoever may author a component may author a route, one that attaches to a Gateway of another namespace or sends traffic to a backend of another namespace included (below).
Authored. The properties are the top-level json fields of the spec type, those of the
CommonRouteSpecit inlines included, decoded strictly at every depth: an unknown key is refused wherever it sits (a parent, a rule, a backend).tcproute(TCPRouteSpec) andudproute(UDPRouteSpec):parentRefs,useDefaultGatewaysandrules. The API accepts exactly one rule.tlsroute(TLSRouteSpec): the same three andhostnames, the server names (SNI) of the TLS handshakes the route takes. A name may start with the wildcard label*.. The route holds no certificate and no key: by the Gateway API’s documentation the listener it attaches to passes the connection through or terminates it.useDefaultGatewaysis an experimental-channel field: the Go types hold it and the standard channel’s CRDs do not, so a cluster on that channel does not keep it. It is the only such field of these specs at v1.6.3, andTestGatewayKinds_RequiredMatchCRDholds that.- No default is filled. The defaults are the CRDs’, and the API server
applies them to what the object leaves out: the
groupandkindof a parent (a Gateway), and thegroup,kindandweightof a backend (a core Service, weight 1).
Required, by the rule of the infrastructure kinds above, is the
nameof a parent and of a backend that are authored (parentRefs[0].name: required (…)): the API requires it and the type would write it empty.A required field the type omits is refused by the kind itself, absent or authored empty, as the API server refuses the object:
- a route with no
rules(rules: required (…)); - a rule with no
backendRefs(rules[0].backendRefs: required (…)); - a
tlsroutewith nohostnames(hostnames: required (…)).
The
httproutekind refuses none of these: the HTTPRoute API requires no rule, no backend and no host name.A Service backend must name its port (
rules[0].backendRefs[1].port: required (…)). The CRDs write that as an expression rule,(size(self.group) == 0 && self.kind == 'Service') ? has(self.port) : true, which the API server evaluates after it has filled the defaults: a backend that names nogroupand nokindis a Service, and so is one that names the core group or the kind alone. A backend of another group or kind needs no port. It is the one expression rule the kinds hold, since it asks for a field that was left out, and the rule reads the same in both channels and on the three kinds.TestGatewayKinds_RequiredMatchCRDholds each required list (2 paths) and the refused omissions to the CRDs the linked module ships, as it does for the infrastructure kinds.TestGatewayRouteKinds_OmissionsAreTheAPIServersandTestGatewayRouteKinds_ExpressionRulesgo one step further: they run the CRD of each channel through the API server’s own creation path (defaulting, the schema, the expression rules), and hold each refusal of a kind to a refusal of that path, and the object the kind emits for an accepted route to being accepted there. The second names every expression rule a CRD declares: the one above, which it shows with routes that break it and routes that keep it, or a rule left to the API server with its reason. A dependency bump that adds or rewrites a rule fails there. Not refused:- more than one rule, more than 16 backends in a rule, and every other value rule of the CRDs: lengths, patterns, minima, maxima and item limits;
- the two expression rules on
parentRefs, which tell two references to one parent apart (bysectionName, and in the experimental channel byporttoo). The channels write them differently, so a refusal by the kind would be wider than one of them; - the three expression rules on a TLSRoute’s
hostnames(a name is no IP address, it is a DNS name, and a wildcard is its first label alone): they hold the form of a value that is authored, as a pattern does; - what a Gateway API controller refuses when it reads the object, which shows in the object’s status, not at creation.
A route is an authored object, as an
httprouteis. A parent and a backend are references carried as written; launcher points neither at a component and does not look for the target in the document. To refer to another component’s object, name it: the component name, or itsobjectName.- References across namespaces are not gated.
parentRefs[].namespaceandbackendRefs[].namespaceare written as authored, on the terms of the infrastructure kinds above and as thehttproutekind writes them: by the Gateway API’s documentation a route attaches only where the Gateway’s listener allows routes of its namespace (allowedRoutes), and a backend of another namespace is used only where a ReferenceGrant of that namespace allows it; launcher checks neither. - No NetworkPolicy allow rule. As for
httprouteabove: the NetworkPolicy synthesis reads a routing trait’s traffic sources and target component, which these kinds do not report, so it allows nothing for a route’s backends. An author who wants the allow rule authors the NetworkPolicy. - No parent from a capability.
parentRefsis what the author wrote; unwritten, the route has no parent.
Policy. No dimension of the environment policy reaches these objects: they run no pod, hold no image, request no storage and have no replica count, and no field of one holds a literal secret. A nil policy and a strict one build the same object. A TLSRoute’s
hostnamesare names the route serves, not artifact sources, and are not held to the policy’s allowed registries, as an HTTPRoute’s are not. The policy’s capability lists gate trait types, so none of them refuses a component of these types; a consumer that restricts routing restricts the component types it registers.Labels and annotations are the
labelsandannotationsproperties. Not covered: whether what is referred to exists (a Gateway, a listener, a Service and its port), whether the cluster’s controller implements the route kind, and the object’sstatus, which the controller writes.secretstore, clustersecretstore, externalsecret, clusterexternalsecret (go-kure/launcher#790) are the kind-named projections of four objects of the External Secrets Operator, in its
external-secrets.io/v1API: a SecretStore, a ClusterSecretStore, an ExternalSecret and a ClusterExternalSecret. Each emits that one object, named after the component unlessobjectNamenames it; the handler adds no label, no annotation and no default. Asecretstoreand anexternalsecretare built in the build namespace. Aclustersecretstoreand aclusterexternalsecretare cluster-scoped: the object carries no namespace, whatever namespace the application is built for, and its name is claimed cluster-wide. Nothing else is emitted: no Secret, ServiceAccount or generator a store or an external secret names, no store an external secret reads from, and not the ExternalSecrets a ClusterExternalSecret has the operator create. All four are built onpolicyHeldKind: the stores for the credentials a provider takes as a value, the two external-secret kinds for the one field that has the operator write another kind than a Secret (target.manifest, under Policy below).No capability is required, and none gates these kinds. Launcher does not ask whether the cluster runs the operator: where the CRDs are not installed the component builds, and the object is refused at apply. The cluster’s
external-secretcapability is the trait’s, and these kinds do not read it. Whoever may author a component may author these, a store that every namespace reads from and an external secret created in every selected namespace included. A policy can keep the kinds out of a build through the object kind policy (oam.ObjectKindPolicy, go-kure/launcher#922).Authored. The properties are the top-level json fields of the spec type, decoded strictly at every depth: an unknown key is refused wherever it sits (a provider, an authentication method, a
dataentry, a template).secretstoreandclustersecretstore(SecretStoreSpec, which the two objects share):provider, with one provider under its key (aws,vault,kubernetes,webhookand the others the API lists),controller,retrySettings(maxRetries,retryInterval),refreshIntervalandconditions(namespaceSelector,namespaces,namespaceRegexes). The operator readsconditionson a ClusterSecretStore only; asecretstorethat authors them carries them.externalsecret(ExternalSecretSpec):secretStoreRef(name,kind),target(name,creationPolicy,deletionPolicy,immutable,template,manifest),refreshPolicy,refreshInterval,syncWindows(kind,windows),data(each asecretKey, aremoteRefand asourceRef) anddataFrom(each anextract, afind, arewriteand asourceRef).clusterexternalsecret(ClusterExternalSecretSpec):externalSecretSpec(the fields of anexternalsecret),externalSecretName,externalSecretMetadata(labels,annotations),namespaceSelector,namespaceSelectors,namespacesandrefreshTime.- A duration is written in the form the API type gives it:
refreshInterval: 1his emitted as1h0m0s, and so are a sync window’sdurationand aclusterexternalsecret’srefreshTime. - No default is filled. The API’s defaults (an external secret’s
refreshInterval, a target’screationPolicyanddeletionPolicy) are the API server’s to fill where the field was not authored. Thekindof asecretStoreRefhas no default in the API: unset, it stays unset in the object, and is read as a SecretStore. - No CRD default, read from its marker, replaces an authored
0orfalse, except on three store fields, where the value is refused. A default the operator applies itself is not covered. The type omits a zero inprovider.beyondtrust.server.decrypt,provider.infisical.secretsScope.expandSecretReferencesandprovider.onepasswordSDK.cache.maxSize, and the API defaults each to another value (true,true,100), so the authored value would be replaced:provider.infisical.secretsScope.expandSecretReferences: false cannot be carried by the external-secrets API types (the field is omitted when zero, so the API server would apply its default true).TestExternalSecretsKinds_DefaultedZerosholds that list to the default markers of the linked module’s source in both directions: no other number or boolean of the four specs is of that shape.
Required follows the rule of the Prometheus operator’s kinds: a field the API requires that the Go type writes whether or not it was authored. Each must be authored; an authored empty value is a value, and the API server’s to refuse. A required field under a parent the author left out is not asked for: the list follows what was authored.
- A store’s
provider, and inside the provider that is authored everything the API requires of it. The list is not written by hand: it is generated from the types of the linked module (zz_generated_externalsecrets_required.go), 263 paths at this pin, over every provider the API has.provider.vault.server,provider.aws.region,provider.aws.service,provider.webhook.urlandprovider.kubernetes.auth.serviceAccount.nameare five of them. The refusal names the path and says which of two things it is:provider.vault.server: required (the external-secrets API requires this field), and for thenameof a referenceprovider.vault.caProvider.name: required (the external-secrets API requires the name of what this refers to). - Four fields of a store the API would default, and the type always
writes. A default is applied to an absent field, and the object carries
an empty one: the API’s enumeration refuses an empty
version, and an empty mount path or a cache lifetime of zero is not what the default would have been. So each must be written where its provider is authored:provider.vault.versionandprovider.openBao.version(the API’s default isv2),provider.vault.auth.cert.path(cert) andprovider.onepasswordSDK.cache.ttl(5m). A Vault store therefore writes itsversion:provider.vault.version: required (the object always carries this field, so the external-secrets API's default v2 never applies: write the value). The four are generated into the same file, in a list of their own. - Of a match expression authored in a condition’s
namespaceSelector: itskeyandoperator. Presence only: the operator’s value and itsvaluesare left to the API server (see “A label selector’s match expressions”). 2 paths, beside the generated ones. - An
externalsecrethas no required top-level field. Of what is authored: adataentry’ssecretKey,remoteRefand itskey; thekeyof anextract; thesourceandtargetof arewrite’sregexpand thetemplateof itstransform; thekindandnameof ageneratorRef; ofsyncWindowsitskindandwindows, and a window’sscheduleandduration; of atarget.manifestitsapiVersionandkind; of a ConfigMap or Secret a template is read from (target.template.templateFrom[]) itsname, itsitemsand an item’skey. 23 paths. - A
clusterexternalsecret:externalSecretSpec, and under it everything anexternalsecretrequires; and thekeyandoperatorof a match expression authored innamespaceSelectoror in an entry ofnamespaceSelectors, presence only. 28 paths. - The published schema marks the two top-level ones as required
(go-kure/launcher#790):
providerof asecretstoreand of aclustersecretstore, andexternalSecretSpecof aclusterexternalsecret. That refuses no component that built before, since the kind refused each without it already.
TestExternalSecretsKinds_RequiredMatchSourceholds the three lists to the source of the linked module, which ships no CRD: the markers its CRDs are generated from are read from the Go files. A field counts as required where its json tag has noomitemptyand it carries no optional marker, and as written unauthored where the Go type encodes it when it is unset. Of the Kubernetes types the specs embed, only a match expression’skeyandoperatorare derived, by the same rule on the source of the linkedmetav1.LabelSelectorRequirement; every other required field of such a type is not refused, and an omitted one is emitted empty.TestExternalSecretsKinds_StoreRequiredIsGeneratedfails where the generated file and that derivation differ, in either direction and for either list, and names the path; a dependency bump that adds, drops or moves one fails there. Regenerate withUPDATE_EXTERNALSECRETS_REQUIRED=1 go test ./pkg/oam/builtin/components/ -run TestExternalSecretsKinds_StoreRequiredIsGenerated. No field of the four specs is required and omitted by the type when unauthored at this pin; the same test fails on the first one a bump brings.The API’s count and expression rules. Every rule the source declares on a type these specs reach is classified in
TestExternalSecretsKinds_Rules, which fails on one that is added or reworded. Checked:- a store configures exactly one provider (
provider: configures no provider; the API takes exactly one;provider: configures 2 providers (aws, vault); the API takes exactly one). A provider key written asnullconfigures nothing, as on the API server; - a generator is not named as the source of one key of
data. The API takes exactly one ofstoreRefandgeneratorRefin thatsourceRef, and the type writesstoreRefwhether or not it was authored (data[0].sourceRef.generatorRef: cannot be carried: the API takes exactly one of storeRef and generatorRef here, and the object always carries a storeRef; read from a generator with dataFrom[].sourceRef.generatorRef). A source ofdataFromtakes a generator. On aclusterexternalsecretboth paths are underexternalSecretSpec.
Not checked, and the API server’s to refuse:
- the count rule on ten other types, each “exactly one of these fields”: a
rewrite, thesourceRefof adataFromentry, and eight blocks of single providers (an authentication method, a reference, how a Yandex provider fetches an entry). The minimum of one on adataentry’ssourceRefis met by thestoreRefthe object always carries; - the six counts over named fields, on three providers:
openBao(at most one ofcaBundleandcaProvider; exactly one authentication method underauth; exactly one ofroleIdandroleRefon an app role; exactly one ofserviceAccountRefandsecretRefon its Kubernetes authentication),onepasswordSDK(at most one ofvaultandenvironment) andcrd(at most one ofauthandauthRef). The type omits each of those fields when unauthored, so the object carries what was authored and no more; - the thirteen rules written as expressions, each on one provider: Barbican
(four), Pulumi (two), Secret Server (two), AWS,
crd, Doppler, GitHub and Nebius; - every other value rule of the API: enumerations, patterns, lengths and minima;
- the rules of the operator’s validating webhook, which refuses more
than the CRDs do: an external secret with neither
datanordataFrom, twodataentries with onesecretKey, adeletionPolicyitscreationPolicydoes not allow, a template of a bootstrap-token Secret or of a service-account-token Secret that names its service account, adataFromentry that sets none or several of its sources; a store whosenamespaceRegexesdo not compile, whoserefreshIntervalis no duration, or whose provider refuses its own configuration. Launcher repeats none of them, so anexternalsecretwith no property builds, and is refused at apply where the webhook runs.
What the type writes unauthored. An empty block,
{}, where the type holds one by value and the API does not require it: an external secret’ssecretStoreRefandtarget, aclusterexternalsecret’sexternalSecretMetadata, an AWS provider’sauthand a Vault provider’stls, among others. The API accepts each.TestExternalSecretsKinds_WrittenUnauthoredpins those five.Policy.
- A credential written into a store is refused under a policy that
forbids explicit secrets. A few providers take a credential as a
valuebeside thesecretRefthat names a Secret holding it, and the object is in the build’s output:provider.delinea.clientSecret.value: holds the credential in the object, and the environment policy forbids explicit secrets; name the key of a Secret created out of band in provider.delinea.clientSecret.secretRef instead. The message quotes nothing of the value. Refused:provider.beyondtrust.auth.apiKey,.auth.certificateKeyand.auth.clientSecret,provider.delinea.clientSecret,provider.scaleway.secretKey,provider.secretserver.passwordand.token. A policy that allows explicit secrets, one that does not answer the question and no policy build it. - The same union holds an identifier on seven other fields, which are not
refused:
provider.barbican.auth.applicationCredentialIDand.auth.username,provider.beyondtrust.auth.certificateand.auth.clientId,provider.delinea.clientId,provider.scaleway.accessKeyandprovider.secretserver.username.TestExternalSecretsKinds_InlineValuesfinds every stringvaluebeside asecretRefin the linked module (14 at this pin) and fails on one that is in neither list. - The data of the
fakeprovider is refused under the same policy.provider.fake.datais nothing but the values the store serves, and the provider has no reference to a Secret. The API also requires the field, and the two refusals come in this order: afakeprovider with nodatais refused as a required field, under every policy and under none; one with entries is refused as an explicit secret under a policy that forbids them;data: []passes both.TestSecretStoreKinds_FakeDatapins the order. - Text in which a reference and a literal look alike is not checked, on
any of the four and under any policy: a header or the body of a
webhookprovider’s request, a header of avaultprovider, the user part of a provider’s URL, and the template of the Secret an external secret writes (target.template.data, atemplateFromliteral, the template’s metadata). A secret written there is in the build’s output. - No field of an external secret is a credential, and one field is
checked:
target.manifest. Anexternalsecretand aclusterexternalsecretname what is read and where it is written, andApplyPolicyfills nothing on either. Withtarget.manifest(anapiVersionand akind) the operator writes an object of that kind instead of a Secret, from template text it renders in the cluster. TheapiVersionhas to be an API version, a version (v1) or a group and a version (apps/v1), each part a name the API can have: the version a DNS-1035 label and the group a DNS-1123 subdomain, the bounds a CustomResourceDefinition’s group and version names have. The core group is written without a slash, so/v1,apps/andApps/v1are none. The API bounds the field by a minimum length only, nothing can write an object of a version that is none, and where the value does not split into the two its kind would read as no kind at all to the check below. One that is none is refused under every policy, and not as a policy refusal (target.manifest.apiVersion: "apps/v1/x" is no API version: want a version (v1) or a group and a version (apps/v1)). That object gets no more than the same policy gives the same kind onpassthrough. Whatpassthroughwould read cannot be read at build, so a kind the rendered-object check reads anything from is refused, in any version and whatever the policy’s own limits: a Pod, PodTemplate, ReplicationController, Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob, PersistentVolumeClaim or PersistentVolume of the built-in groups, and a HorizontalPodAutoscaler:target.manifest: the operator would write a Deployment, a kind the environment policy checks, and what the object would hold is not known at build, so it cannot be checked against environment policy(on aclusterexternalsecretthe path isexternalSecretSpec.target.manifest). The transform appliesNoopPolicywhen no policy is passed, so this holds then too. This is stricter thanpassthroughwherever the policy would pass the object it read: a claim under a policy with no storage maximum, an autoscaler under one with no replica maximum, a workload within every limit.passthroughreads the object and finds nothing to refuse; here the content is not known at build, so there is nothing to read. A coreSecretnamed there is refused under a policy that forbids explicit secrets and passes under any other, as onpassthrough. Any other kind (a ConfigMap, a custom resource) passes as it would onpassthrough, and whether the operator writes it at all is the cluster’s: the operator’s generic-target setting and its RBAC. Thetargetpath of atemplateFromentry needs no check of its own: it places text inside the objecttarget.manifestnames.TestPolicyReadsKind_IsWhatTheRenderedObjectCheckReadsfails when the refused set and the check differ. - Hosts are not checked. A host these objects name is one the operator
reaches, not an artifact source, and none is held to the policy’s allowed
registries: the server, URL, host or endpoint of every provider
(
provider.vault.server,provider.webhook.url,provider.kubernetes.server.url,provider.azurekv.vaultUrl,provider.conjur.url,provider.onepassword.connectHostand the others). - A nil policy checks nothing.
The store of an external secret is the author’s.
secretStoreRefnames a store bynameandkind; launcher points it at no component, fills none from the cluster’s capability and does not look for the store in the document. To have asecretstoreorclustersecretstorecomponent serve anexternalsecretcomponent, name its object: the component name, or itsobjectName. The API reads asecretStoreRefwithout akindas a SecretStore of the ExternalSecret’s namespace. The Secrettargetnames is the operator’s to create, and the component emits none.Labels and annotations are the
labelsandannotationsproperties. The object also carries the component label, whose value is the component’s (see “Component label and ownership” in the OAM model). The labels of the ExternalSecrets a ClusterExternalSecret creates are itsexternalSecretMetadata, and those of the Secret an external secret writes itstarget.template.metadata: the operator applies both, and launcher adds nothing to either.Beside the
external-secrettrait. The trait derives an ExternalSecret for the workload it is attached to, from a few properties and a store it names itself or takes from the cluster’sexternal-secretcapability, names it after itssecretName, and mounts or injects the Secret into the workload. Theexternalsecretkind is the authored object, for what the trait does not express: the whole spec, and an ExternalSecret that belongs to no workload. The two type names differ (external-secret,externalsecret). The two objects are one kind: a trait’s and a component’s given one name in one namespace are refused (generated-object collision: ExternalSecret.external-secrets.io "default/app-credentials" is generated by both …).Not covered. The operator’s other APIs: its generators and its PushSecret. Whether what is referred to exists (a store, a Secret and its key, a ServiceAccount, a ConfigMap, a generator, a namespace), and whether the operator can reach what a store names. The objects’ status is the operator’s and is not written.
replicationsource, replicationdestination (go-kure/launcher#790) are the kind-named projections of the two objects of VolSync’s
volsync.backube/v1alpha1API: a ReplicationSource, which copies a volume out, and a ReplicationDestination, which receives one. Each is built onpolicyHeldKind(policyFreeKindwith a policy check, see above) and emits that one object in the build namespace, named after the component unlessobjectNamenames it, and declares it as namespaced; the handler adds no label, no annotation and no default. The two share their movers but for Syncthing, and what is said of a mover below holds for both kinds.No capability is required, and none gates these kinds. Launcher does not ask whether the cluster serves
volsync.backube/v1alpha1: where VolSync’s CRDs are not installed the component builds, and the object is refused at apply. A policy can keep the kinds out of a build through the object kind policy (oam.ObjectKindPolicy, go-kure/launcher#922). The kinds read nothing of the cluster profile; thevolsynctrait still takes its class defaults from it.Authored. The properties are the top-level json fields of the spec type, decoded strictly at every depth: an unknown key is refused wherever it sits (a mover, its security context, a volume mounted into it).
replicationsource(ReplicationSourceSpec):sourcePVC, the claim to copy;trigger(schedule, a cron expression, ormanual); the moversrsync,rsyncTLS,rclone,resticandsyncthing;external, a provider outside VolSync with itsparameters; andpaused.replicationdestination(ReplicationDestinationSpec):trigger, the moversrsync,rsyncTLS,rcloneandrestic,externalandpaused. A destination has no Syncthing mover and names no source claim:syncthingandsourcePVCare refused there as unknown keys.rsync(rsync over SSH) has its own, shorter type: it takesmoverResources,moverServiceAccountandmoverPodLabels, and has nomoverSecurityContext,moverAffinityormoverVolumes, which are refused under it as unknown keys.- A mover authored empty is in the object, empty but for what the type writes unauthored (below); one left out is not.
- An authored
falseor0is kept where the API tells it from an unset field (restic.retain.monthly: 0, a destination’srestic.previous: 0, a Syncthing peer’sintroducer: false,moverSecurityContext.runAsUser: 0), and left out where the type omits a zero (paused: false). VolSync’s CRDs default no field underspec, so no authored zero is replaced by a CRD default;TestVolsyncKinds_NoDefaultsholds the linked CRDs to that, and a dependency bump that adds a default fails there with the field named.
Required follows the rule of the Prometheus operator’s kinds: a field the API requires that the Go type writes whether or not it was authored must be authored. Neither kind has one at the top level.
- Of a volume mounted into a mover (
moverVolumes, onrsyncTLS,rclone,resticandsyncthing):mountPathandvolumeSource(restic.moverVolumes[0].volumeSource: required (…)). - Of a Syncthing peer:
address,IDandintroducer. No default is filled forintroducer: an authoredfalseis a value. - Of a mover’s affinity that authors a required node affinity
(
moverAffinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution): itsnodeSelectorTerms, which the Kubernetes type would write asnullwhere the CRD requires them.TestKindComponents_NullRequiredshows the refusal with the validator of the linked CRDs. - Of a match expression that is authored in a label selector of a mover’s
pod affinity or anti-affinity (
moverAffinity, onrsyncTLS,rclone,resticandsyncthing):keyandoperator. Presence only: the operator’s value and itsvaluesare left to the API server (see “A label selector’s match expressions”). - A required field under a parent the author left out is not asked for: the list follows what was authored.
TestVolsyncKinds_RequiredMatchCRDholds the lists (75 paths for a source, 54 for a destination) to the CRDs the linked module ships: every field of VolSync’s own types that a CRD requires and the type writes unauthored is listed, and so is thekeyandoperatorof a match expression, and nothing else is. A dependency bump that adds, drops or moves one fails there. Not refused:- a required field the type omits when it is not authored: the object shows the omission, and the API server refuses it;
- a required field of a Kubernetes type these specs embed, other than those
terms and a match expression’s two fields (the
keyof a node selector requirement in an affinity, for one). An omitted one is emitted empty; - every other value rule of the CRDs (enumerations, patterns, minima).
Two fields the linked Kubernetes type holds and the CRDs do not are refused when authored:
<mover>.moverVolumes[].volumeSource.secret.defaultUserand<mover>.moverVolumes[].volumeSource.secret.items[].user, an authored0included (rclone.moverVolumes[0].volumeSource.secret.defaultUser: no field of the volsync.backube/v1alpha1 API: its CRD has no such property). The linked Kubernetes API is newer than the one VolSync’s CRDs were generated from: the Go type of a mounted Secret has the two fields, the strict decode reads the Go type, and the CRDs have no property for either.TestVolsyncKinds_AbsentFromCRDderives the fields the types reach that the linked CRDs have no property for and holds the refusals to them, both ways: a bump of either module that closes the gap, or widens it, fails there with the field named. It also runs the API server’s own create sequence on the linked CRDs (crdCreate): the API server names each of the two as an unknown field, for which a request made with strict field validation is refused, and prunes it from the object it goes on with.The CRDs declare no expression rule (
x-kubernetes-validations), so the kinds check none;TestVolsyncKinds_NoExpressionRulesholds the linked CRDs to that, and a dependency bump that adds one fails there with the rule named.What the type writes unauthored.
customCA: {}on an authoredrcloneorresticmover, which holds it by value. The CRDs accept it empty, andTestVolsyncKinds_RequiredMatchCRDholds every such field to its schema.Policy.
- An authored capacity is held to the storage maximum. A mover’s
capacity(onrsync,rsyncTLS,rcloneandrestic) sizes a volume the operator provisions: on a source the point-in-time image of the volume, on a destination the volume the data is received into.restic.cacheCapacitysizes Restic’s metadata cache volume and, on a source,syncthing.configCapacitySyncthing’s configuration volume. Each is held to the environment policy’s storage maximum:component "web": restic.capacity "20Gi" exceeds enforced maximum "10Gi". A capacity the author left out is the operator’s to choose and is not held; the policy’s default storage size is not filled. - A mover’s cpu and memory are held to the maxima. The limits and
requests of
moverResourcesare held to the policy’s cpu and memory maxima, as a container’s are, on every mover:component "web": rsync.moverResources: cpu limit "4" exceeds enforced maximum "2". No default is filled: the policy’s default requests and limits are a workload’s. A request above its limit is not refused here. - A mover’s
hostProcessswitch is refused unless the policy allows privileged workloads.moverSecurityContext.windowsOptions.hostProcess: trueruns the mover’s containers as Windows HostProcess containers:component "web": restic.moverSecurityContext.windowsOptions.hostProcess is not allowed by environment policy. An authoredfalsebuilds. With no policy passed it is refused too, sinceNoopPolicyallows nothing privileged. - The
rsyncmover is held to the policy’s container capabilities. Authoring the rsync-over-SSH mover is itself a choice of privilege: the linked operator version (VolSync v0.16.0) runs its container as root (runAsUser: 0), not privileged, with every capability dropped and seven added (AUDIT_WRITE,CHOWN,DAC_OVERRIDE,FOWNER,SETGID,SETUID,SYS_CHROOT), on a source and on a destination alike, and its builder does not take the namespace’s answer on privileged movers. An authoredrsyncmover, whatever it holds, is therefore held to the policy’s allowed and forbidden container capabilities for those seven, as a container that adds them is on a pod kind:component "web": rsync: the mover's container as VolSync v0.16.0 writes it: securityContext.capabilities.add: "SYS_CHROOT" is forbidden by environment policy. A policy that sets neither list, and no policy, build it; allowing privileged workloads does not lift a forbidden capability, as it does not on a pod kind. Two limits. The seven are what the linked operator version writes (internal/controller/mover/rsync/mover.goin its module): a cluster that runs another version of the operator may add other capabilities, and the kind does not know.TestVolsyncKinds_RsyncCapabilitiesreads that literal and holds the non-test Go source of the two packages the container is built in (internal/controller/mover/rsyncandinternal/controller/utils) to a stated digest: a dependency bump that changes either package fails, whatever the change, until a person has re-read the mover and restated the digest. The test cannot check that the re-reading was done; a bump that touches either package needs it even when the container is untouched; and the module’s other packages and the functions of other modules that the mover calls are outside the digest. And that the container runs as root is stated here, not held: the policy has no dimension for it. - Not held: the rest of
moverSecurityContext. It is a pod security context, and of it onlywindowsOptions.hostProcessis held: the user and groups the mover runs as, its sysctls and its SELinux and seccomp settings are written as authored, since the environment policy has no dimension for them. The service account the mover runs under (moverServiceAccount, on every mover) is an identity carried as authored. Nor are held a mover’s affinity; the volumes mounted into it (moverVolumes: a Secret, a claim or an NFS export; the type holds no host path); the type of the Service a mover is reached through (serviceType); and whether anrsyncTLS,rclone,resticorsyncthingmover runs with elevated permissions, which is no field of the object: an administrator sets it with an annotation on the namespace (volsync.backube/privileged-movers), which the linked operator’s controllers read and hand to the builder of each of those four movers. Thersyncmover’s builder drops that answer (above). - Hosts are not checked. A host these objects name is one the mover
reaches, not an artifact source, and none is held to the policy’s allowed
registries:
rsync.address,rsyncTLS.address, a Syncthing peer’saddress, and theserverof an NFS export amongmoverVolumes. - No field holds a literal secret by design, and none is checked. A
credential is the name of a Secret (
restic.repository,rclone.rcloneConfig,rsync.sshKeys,rsyncTLS.keySecret). Theparametersof anexternalprovider are a map of strings the provider reads: they are written as authored and not checked, under a policy that forbids explicit secrets too.
The operator’s own rule is not repeated. When it reconciles, VolSync refuses an object that configures no replication method or more than one (
a replication method must be specified,only one replication method can be supplied). Launcher does not: such a component builds, and the rule stays the operator’s.Beside the
volsynctrait. The trait derives a ReplicationSource for a claim of the workload it is attached to, a Restic backup on a schedule, and names it<sourcePVC>-backup. Thereplicationsourcekind is the authored object, for what the trait does not express: the whole spec, any mover, a source that belongs to no workload. No trait builds a ReplicationDestination. The trait’s object and areplicationsourcecomponent’s are one kind: given one name in one namespace they are refused (generated-object collision: ReplicationSource.volsync.backube "default/data-backup" is generated by both …).Labels and annotations are the
labelsandannotationsproperties. A mover’smoverPodLabelsare not these: they are labels the operator adds to the mover pods. They are held as a pod’s labels are: the component label’s key holds the component’s value there or the build is refused, and a key the consumer reserved is refused (mover pod label "…" may not be set); see “Component label and ownership” and “Reserved metadata keys” in the OAM model. Launcher writes nothing there. A destination’sserviceAnnotationsreach a Service and are not read.Not covered. Whether what is referred to exists (the claim to copy, a Secret, a storage or snapshot class, a ServiceAccount), and whether the mover can reach what the object names. The object’s status is the operator’s and is not written.
statefulset —
serviceNameandvolumeClaimTemplates(name,mountPathor — for avolumeMode: Blockclaim —devicePath,size,storageClass,accessModes, plus the rest ofcorev1.PersistentVolumeClaimSpec; see “Raw block volumes” above). The StatefulSetSpec-level and claim-template field sets are classified in “StatefulSet-level and claim-template properties” below.portsdeclares the main container’s ports (see “Main container ports”). It emits no Service (go-kure/launcher#690). For scheduling it takesaffinity,tolerationsandtopologySpreadConstraintsas the rawcorev1shapes (go-kure/launcher#790, see “Raw scheduling properties” above); the four-keyaffinityshorthand is not published on this kind.serviceNamenames the governing Service, which the component does not emit. Author it as a headlessservicecomponent (clusterIP: None, selectingapp: <component>) and name it here. It has no default: unset, the StatefulSet carriesserviceName: "". An authored value must be a valid Service name, a DNS-1035 label (at most 63 characters, lowercase letters, digits and-, starting with a letter and ending with a letter or digit), and is refused at conversion otherwise (serviceName: "api.v1" is not a valid Service name, which must be a DNS-1035 label, go-kure/launcher#546);Generateapplies the same rule to the config’sServiceName. Before go-kure/launcher#690serviceNamedefaulted to the component name and the component emitted that headless Service itself; “Main container ports” has the migration, including why an existing StatefulSet must be recreated.- A route to the pods names the authored Service. With no Service of
its own, the component is no implicit backend: an
ingress,httprouteorexposewithout abackendon it is refused (component "<name>" has no service port). Put the trait on the authoredservice, or name that Service as the trait’sbackend.
daemonset —
tolerations(key/operator/value/effect/tolerationSeconds;tolerationSecondsand the toleration cross-field rules arrived with go-kure/launcher#412 via the shared parser — see “Whattolerationschanged fordaemonset” above, which is the only place in this work that is not additive);portsdeclares the main container’s ports (see “Main container ports”). It emits no Service (go-kure/launcher#690): author aservicecomponent selectingapp: <component>to expose the pods, and put routing traits on it. Since go-kure/launcher#790 it also publishes the rawaffinityandtopologySpreadConstraints(see “Raw scheduling properties”) andsidecars, read, checked and enforced exactly as onstatefulset: the same closed entry, the same pod-wide port-name check against the main container’sports, and the sameApplyPolicyloop. A sidecar here runs on every node the DaemonSet schedules to. DaemonSetSpec-level (go-kure/launcher#340,daemonset_spec.go):updateStrategy,minReadySeconds,revisionHistoryLimit.appsv1.DaemonSetSpechas five fields;templateis the pod projection above andselectoris builder-managed, which leaves these three.Before go-kure/launcher#690,
port > 0added a ClusterIP Service named after the component, which held the component name to the Service-name rule and an ingressportNametohttp(go-kure/launcher#545, go-kure/launcher#546). With no Service, only the container-name rule applies (see webservice / worker).Property Type Effect Compatibility updateStrategyobject type(required,RollingUpdate|OnDelete) androllingUpdate(maxUnavailable,maxSurge), only accepted undertype: RollingUpdate.type: RollingUpdatealone is accepted — the apiserver defaults therollingUpdateobject that upstream validation then requires.additive minReadySecondsint ≥ 0 Seconds a new pod must stay ready before it counts as available. additive revisionHistoryLimitint ≥ 0 Superseded ControllerRevisions kept for rollback. additive selector— Rejected outright, not silently dropped: the selector is builder-managed ( app: <component>), must equal the generated template labels, and is immutable once created.Behavior-changing template— Rejected outright (go-kure/launcher#790), as on deployment: the pod template is projected from the component’s own container and pod-level properties. A document was refused already, by the authored-property check; the parser now refuses it too, where a caller that skips that check had it dropped.Behavior-changing for such a caller maxUnavailableandmaxSurgeeach accept a non-negative integer or a"N%"string with N ≤ 100 (the integer form is a pod count and is deliberately uncapped, matching upstream’sIsNotMoreThan100Percent, which inspects only percentages). A leading sign ("+50%") is rejected — upstream’s ownIsValidPercentform check is used rather than aTrimSuffix/Atoipair, which would accept it. Unlike the statefulset kind, whosemaxUnavailable“cannot be 0”, either DaemonSet knob may be zero on its own; it is the pair that must have exactly one non-zero member. That rule is enforced against the effective pair, counting the API defaults for whichever half the document leaves out —maxUnavailable1,maxSurge0 (k8s.io/api/apps/v1/types.go, theRollingUpdateDaemonSetfield docs). SomaxSurge: 2on its own is rejected here rather than at apply time (the defaultedmaxUnavailable: 1makes both non-zero), and using surge means writingmaxUnavailable: 0alongside it. The error names which half was defaulted, since that is the half absent from the author’s YAML.Both knobs are published as the
Types: [integer, string]union (go-kure/launcher#383), so the integer and the percentage form both pass property validation on the authored and the emitted path alike, and a value of any other type is rejected there before the parser sees it. The union leavesTypeempty, so a schema consumer that does not readTypesstill accepts both forms.Two rules here are deliberately stricter than upstream. The API accepts both shapes; what it does with them differs.
updateStrategy.typeis required here, where the API defaults it toRollingUpdateand acts on that — so a bareupdateStrategy: {}is a legal document whose entire meaning comes from defaulting rather than from anything written.updateStrategy.rollingUpdateis refused undertype: OnDelete, whereValidateDaemonSetUpdateStrategy’sOnDeletebranch is empty and the field is accepted and never read — the silently-ignored knob this projection exists to remove. Both are still additive:updateStrategyis a new property, so no document that built before this change can carry either shape. In the other direction the parser is laxer in exactly one place — upstream requires a non-nilrollingUpdateunderRollingUpdate, but apiserver defaulting satisfies that, not the author, sotype: RollingUpdatealone is accepted (the same reasoning the statefulset kind applies to its own optionalrollingUpdate).Every accepted property is presence-gated: a document authoring none of them produces byte-identical output to before, because
DaemonSetSpecConfig.applywrites only the fields that were authored. The one behavior change isselector: aselector:on a daemonset used to be silently ignored — before go-kure/launcher#408 (closed), authored-document validation checked type names and identity, not property shape — and now fails the build, both becausedaemonsetdoes not declare the key and becauseValidateAuthoredProperties(pkg/cmd/kurel/build.go:149) now rejects an undeclared authored key on its own. That is the point — a silently dropped selector reads as applied.It is a behavior change against what the code did, not against what the format promised.
docs/oam/design-gvk.md§ Parser Strictness already states the contract — “Launcher rejects unknown fields in all launcher-native documents. Anapp.yaml,kurel.yaml, orcluster.yamlwith unrecognised keys is a build error” — so a daemonset carryingselector:was never a validlauncher.gokure.dev/v1alpha1document, and the same-version stability promise (“every document that was valid under it remains valid”) never covered it. The authored path simply had not implemented that strictness for this key; the rejection implements it, with a message that says why rather than the generic unknown-key error.6aed090(feat(format): enforce policy and support securityContext on init/sidecar containers) is the in-repo precedent for shipping this class of tightening under an unchanged version string.cronjob —
scheduleaccepts a standard 5-field cron expression (e.g.0 2 * * *; not 6-field — a seconds field is rejected), one of the fixed@-descriptors (@yearly,@annually,@monthly,@weekly,@daily,@midnight,@hourly;@rebootis rejected, meaningless for a CronJob), or@every <duration>(e.g.@every 1h30m, validated via Go’stime.ParseDuration— a malformed duration is rejected, not merely regex-matched).restartPolicy(defaultOnFailure),successfulJobsHistoryLimit/failedJobsHistoryLimit.portsdeclares container ports, and emits no Service (see “Main container ports”). Nosidecarsschema key (init containers only): a plain sidecar keeps running after the main container exits, so the Job’s pod would never complete. The container that fits is the restartable init container, which this package does not model (see “Container fields”). Since go-kure/launcher#790 it publishes the rawaffinity,tolerationsandtopologySpreadConstraints(see “Raw scheduling properties”), written onto the Job template’s pod template. CronJobSpec-level:concurrencyPolicy(Allow|Forbid|Replace; the API’s own default isAllow, but this is only ever written when authored —ConcurrencyPolicyhas noomitempty, so writing it unconditionally would add the key to every generated CronJob),suspend,startingDeadlineSeconds(>= 0),timeZone(a real IANA zone name, e.g.Europe/Brussels; an authored empty string is rejected outright,Localis rejected case-insensitively even though Go’s owntime.LoadLocation("Local")succeeds — Kubernetes’ CronJob validation rejects it as server-dependent — and any other value is checked viatime.LoadLocation, which this binary resolves from an embedded IANA database rather than the host’s zoneinfo). JobSpec-level (projected ontospec.jobTemplate.spec, shared with thejobcomponent — the full table is under job, and the shared trio is described in “Common config” above):backoffLimit,completions,parallelism,activeDeadlineSeconds,ttlSecondsAfterFinished,completionMode,backoffLimitPerIndex,maxFailedIndexes,podReplacementPolicy,managedBy,successPolicy,podFailurePolicy.suspendon a cronjob is the CronJobSpec field, not the JobSpec one — see thesuspendnote in “Common config”. Every CronJobSpec-level and JobSpec-level field above exceptrestartPolicyand the two history limits is presence-gated: omitting it never adds a key to the generated output, even where the corresponding Kubernetes default (e.g.concurrencyPolicy: Allow) would otherwise appear to have been authored. Those three are always written, takingOnFailure, 3 and 1 when omitted. The CronJob’s name is at most 52 characters, the longest the API server creates one under (ValidateCronJobCreate: the controller names each Job after the CronJob plus an 11-character suffix, and that Job’s name is held to 63). The rule is held on the name the CronJob takes, the component name or itsobjectName(or theNaminghook’s answer), when the component is read and again atGenerate:cronjob "<name>": the component name is the CronJob's name, which must be at most 52 characters: it has 53, orobjectName (or the Naming hook's answer for role "object"): "<name>" is not a valid CronJob name, which must be at most 52 characters: it has 53. A component named with 53 to 63 characters built before this rule and is refused now (go-kure/launcher#787); a longer one was already refused by the container-name rule. With an Indexed job template the name must also leave room for the index in each pod’s hostname, a limit that applies when the controller creates the Job, not when the CronJob is admitted; the build does not check it. Known limitation: the plain 5-fieldscheduleform accepts any 5 whitespace-separated tokens with no per-field semantic check (e.g.99 99 99 99 99builds successfully here and is only rejected later, by Kubernetes’ own API server) — this is deliberate, not an oversight: tightening it would reject documents that build successfully today, which is a breaking change under this project’s additive-compatibility rule (seedocs/oam/design-gvk.md).job (go-kure/launcher#344,
job.go) — a run-to-completion workload:image,restartPolicy(defaultOnFailure; the Job API rejectsAlways, which is what pod defaulting would otherwise supply, and a present-but- non-string or empty value is refused rather than read as an omission that would silently restore that default), and the shared container and pod-level surface above. Noportand no Service;portsdeclares container ports (see “Main container ports”), and only then may a probe or hook name a port; nosidecarsschema key (init containers only), matchingcronjoband for the reason given there. Since go-kure/launcher#790 it publishes the rawaffinity,tolerationsandtopologySpreadConstraints(see “Raw scheduling properties”), written onto the Job’s pod template. It emits aJoband nothing else (go-kure/launcher#702).The Job’s name is at most 63 characters: the API server writes it as the value of the pod template’s
job-nameandbatch.kubernetes.io/job-namelabels (generateSelector), and a label value is at most 63. The rule is held on the name the Job takes, the component name or itsobjectName(or theNaminghook’s answer), when the component is read and again atGenerate, in the words thecronjobrule uses. A component name over 63 characters was already refused at generation, by the container-name rule; it is now refused when the component is read, by this one (go-kure/launcher#787).On a job with
completionMode: Indexedandcompletionsabove 0 the name is held to one more rule of the API server’s (validateNameAllowsCompletions): the pod of each index takes the hostname<name>-<index>, so<name>-<completions-1>must be a DNS-1123 label, at most 63 characters and without a dot. It is held at the same two places, and refused asjob "<name>": the component name is the Job's name, and with completionMode Indexed and completions 11 the pod of the last index takes the hostname "<name>-10", which must be a DNS-1123 label: …. An Indexed job whose name leaves no room for its last index built before this rule and is refused now. A document cannot leavecompletionsunset on an Indexed job, but aJobConfigbuilt in Go can: withParallelismunset too the API server reads both as 1, soGenerateholds the name to<name>-0there.The twelve JobSpec-level properties are the ones
cronjobprojects onto its job template, projected here ontospecdirectly. Every one is presence-gated, so a document authoring none of them emits no key for them.Property Type Effect Compatibility backoffLimitint ≥ 0 Retries before the job is marked failed. API default 6, or 2147483647 when backoffLimitPerIndexis set.additive completionsint ≥ 0 Successful pods required. Required under completionMode: Indexed.additive parallelismint ≥ 0 Pods running at once. ≤ 100000 under Indexed; ≤ 10000 whencompletions> 100000 andbackoffLimitPerIndexis set.0runs no pods at all.additive activeDeadlineSecondsint > 0 Seconds the job may run. Must be positive — 0is rejected, not just negatives. Distinct from the pod-levelpodActiveDeadlineSeconds.additive ttlSecondsAfterFinishedint ≥ 0 Seconds after finishing before the job and its pods are deleted. additive completionModeNonIndexed|IndexedHow completions are counted. Indexedrequirescompletions.additive backoffLimitPerIndexint ≥ 0 Retries within one index. Requires Indexed.additive maxFailedIndexesint ≥ 0 Failed indexes tolerated before the whole job fails. Requires backoffLimitPerIndex(and soIndexed); ≤completionsand ≤ 100000, or ≤ 10000 (and required) whencompletions> 100000.additive podReplacementPolicyFailed|TerminatingOrFailedWhen a replacement pod is created. An empty string is rejected rather than treated as unset, for the same reason as managedBybelow.additive managedBystring Controller reconciling this job instead of the built-in one. A domain-prefixed path ( example.com/controller), ≤ 63 characters. An empty string is rejected rather than treated as unset.additive successPolicyobject rules[](1..20) ofsucceededIndexes(increasing comma-separated intervals, every index <completions) and/orsucceededCount(≤completions, and ≤ the number of indexes named alongside it). RequiresIndexed. An emptysucceededIndexesis rejected rather than treated as unset: it denotes no indexes at all, so it would otherwise satisfy the at-least-one-field rule while naming nothing.additive podFailurePolicyobject rules[](0..20), each with anaction(FailJob|FailIndex|Ignore|Count) and exactly one ofonExitCodes(operatorIn|NotIn,values[]of 1..255 exit codes in increasing order without duplicates, optionalcontainerName) oronPodConditions[](up to 20type/statuspatterns; an omitted or nullstatusdefaults toTrue, an empty one is rejected). An emptyonPodConditions: []alone is neither; besideonExitCodesit is both, as the rule’s publishedexclusivegroup counts it (go-kure/launcher#790). RequiresrestartPolicy: Never, and pinspodReplacementPolicytoFailedwhen that is also authored.FailIndexadditionally requiresbackoffLimitPerIndex.Behavior-changing on cronjob(see below); additive onjobsuspendbool JobSpec.Suspend— create the job with no pods. Not the same field ascronjob’ssuspend; see thesuspendnote in “Common config”.additive selector,manualSelector,template— Rejected outright, not silently dropped: the Job selector is generated by the job controller from a unique per-job label, and a hand-written one adopts other jobs’ pods. manualSelectoronly has meaning alongside one.templateis replaced wholesale from the component’s own container and pod-level properties, so an authored one is discarded rather than merged — the same rejection the deployment kind makes.cronjobrefuses the three as well (go-kure/launcher#790): its job is the same JobSpec, and its parser read none of them and said nothing.Behavior-changing (see below) scheduling— Rejected outright on jobandcronjob(go-kure/launcher#790): alpha upstream, behind theWorkloadWithJobfeature gate, and read by neither.Behavior-changing for a caller that skips the authored-property check schedule,timeZone,concurrencyPolicy,startingDeadlineSeconds,successfulJobsHistoryLimit,failedJobsHistoryLimit— Rejected outright: these are the CronJobSpec-only keys, and they are exactly what a cronjobdocument retyped tojobleaves behind. Dropping them silently would run at apply time the work a schedule deferred (see below).additive (no valid jobdocument ever carried them)Every cross-field rule above is ported from
ValidateJobSpec(k8s.io/kubernetes/pkg/apis/batch/validation) at the pinnedk8s.io/apiversion, so a document this parser accepts is one the API server accepts too — the rejections move an apply-time failure to build time. One upstream constraint is deliberately not enforced:BackoffLimitPerIndex’s field doc says per-index backoff only applies when the pod restart policy isNever, butValidateJobSpecdoes not check it and neither does this parser — the schema description carries the constraint instead. Refusing a combination the API server accepts would be this projection inventing a rule, which is a different thing from the daemonset kind’supdateStrategystrictness, where upstream accepts a field and then provably never reads it.Two rejections go the other way — stricter than
ValidateJobSpec, because the field’s own documented contract ink8s.io/api/batch/v1is stricter than the validator that is supposed to enforce it.activeDeadlineSeconds: 0is refused although the validator only requires non-negative, because the field doc says “value must be positive integer”. An emptysuccessPolicy.rules[].succeededIndexesis refused although the validator’s index parser returns “no indexes, no error” for it, because the field doc says “At least one element is required”. Both follow the documented contract rather than the gap in its enforcement. A third,podFailurePolicy.rules[].onPodConditions[].status: "", is covered withpodFailurePolicybelow. These three are called out here because they are the only places a document this parser refuses would in fact have applied.podFailurePolicy(go-kure/launcher#345) is published by bothjobandcronjob, through the same shared parser as the rest of the table. Its rules are ported fromvalidatePodFailurePolicy/validatePodFailurePolicyRule, with the differences below.Two checks run at emission rather than at parse time, in
validateJobPodFailurePolicyAgainstTemplate, because both read the built pod template rather than the property map: upstream requiresrestartPolicy: Neveralongside apodFailurePolicy(these components default it toOnFailure, so a document authoring only the policy is refused with a message naming the restart policy), andonExitCodes.containerNamemust name a container the template actually carries — the component’s own container or one of itsinitContainers. Checking the emitted template rather than the config is what keeps the check from drifting from what is written, the same reason the component-name check increateJobreads the emitted name.One place this parser is deliberately no stricter than upstream: an empty
ruleslist is accepted.validatePodFailurePolicyhas no “at least one rule” check — unlikevalidateSuccessPolicy, which does — andPodFailurePolicy.Rules’ own field doc states only the cap, so unlikeactiveDeadlineSecondsandsucceededIndexesabove there is no documented contract to follow past the validator. An empty list is not inert either: a non-nil policy pinspodReplacementPolicyandrestartPolicywhatever its rules say.onPodConditions[].statusdefaults toTrue(go-kure/launcher#410): a pattern that omits the key, or sets it to null, compiles to an explicitstatus: "True", the value upstream’sSetDefaults_PodFailurePolicyOnPodConditionsPatternfills in before validation. It is written out rather than left to the API server because the field carries noomitempty, so an unset one would be emitted asstatus: "". An authored empty string is still refused, which is deliberately stricter than upstream — its defaulter fills""too — on the reading that an empty value is more likely a templating slip than a request forTrue. The default is additive: it only makes previously refused documents valid, and every previously valid document compiles identically.Compatibility. On
jobthis is additive — the component type is newer than the key. Oncronjobit is behavior-changing, and the reason is worth stating precisely: before this change an authoredpodFailurePolicyon a cronjob was not refused, it was ignored. At the time, property-schema validation ran on emitted elements only, never on authored documents, so a key no handler read was dropped in silence. A cronjob document that was already authoringpodFailurePolicytherefore either compiles to a CronJob that now carries the policy, or stops building — most likely on therestartPolicy: Neverrequirement, since the component defaults toOnFailure. Under the additive test indocs/oam/design-gvk.md(“every previously valid document remains valid and compiles to the same output”) that was not additive. The gap that made this true of every property ever added to an existing kind — the authored path not enforcing the Parser Strictness that document promises — belonged to the repo rather than to this change, and is fixed as go-kure/launcher#408 (closed):kurel buildnow callsValidateAuthoredProperties(pkg/cmd/kurel/build.go:149), which checks declared keys and shapes on the authored document directly.The
selector/manualSelectorrejection is the same class of change the daemonset kind’sselectorrejection is, and rests on the same reasoning:docs/oam/design-gvk.md§ Parser Strictness already promises that unknown fields in a launcher-native document are a build error, so neither key was ever part of a validlauncher.gokure.dev/v1alpha1job. Here it is not even a tightening —jobis a new component type, so no document that built before this change can carry either key. The explicit rejection exists so the message says why: to a caller of the handler, which gets the reason alone, and since go-kure/launcher#790 in a document too, where the authored-property check appends the reason to its refusal of the undeclared key.The six CronJobSpec-only keys —
schedule,timeZone,concurrencyPolicy,startingDeadlineSeconds,successfulJobsHistoryLimitandfailedJobsHistoryLimit— are rejected for the same reasonselectoris, but the case they guard is sharper. Ajobcomponent and acronjobcomponent differ by exactly these six properties, so retyping an existingcronjobdocument tojobleaves every one of them behind. Left undeclared each would be dropped in silence, turning a schedule into a Job that runs once, immediately, at apply time — not a missing field, but work executed at a time nobody asked for. Each message names the job-level property that does the nearest equivalent job where one exists (activeDeadlineSecondsforstartingDeadlineSeconds,ttlSecondsAfterFinishedfor both history limits), so the refusal is a redirection rather than only a “no”.The properties this component introduces read an explicit
nullas an omission:backoffLimitPerIndex: nullmeans “leave this unset”, not “this is a value of the wrong type”. That matchespkg/oam’s ownvalidatePropertyValue. So doescompletionMode, which the sharedparseJobSpecread with a bare lookup until go-kure/launcher#570, oncronjobas well. Since go-kure/launcher#394 this is not special to this component: every optional-field helper incommon.goroutes its presence check throughauthoredValue, so each JobSpec field shared withcronjobthat is read through one of them reads a null the same way on both kinds, and so does a field any other kind reads through one. The change was one-directional — a null that used to be refused is now absence, and nothing that parsed before parses differently — so no document that built before stopped building.The component name must be a DNS-1123 label, not merely a subdomain — a rule this component shares with every workload kind; see “The main container is named after the component” under “Common config” above.
Updating a
jobin place needs it force-replaced, which theforce-replacetrait asks for. A Job’s pod template is immutable:ValidateJobSpecUpdaterunsvalidatePodTemplateUpdateon every update, and the only carve-out is for scheduling directives on a suspended Job. By default, changing this component’simage,command,envor any other pod-level property therefore produces an apply failure on the second delivery, not a re-run — the first apply creates the Job, and every later one is rejected while the Job object still exists. That default is deliberate and unchanged (go-kure/launcher#406): replacing a Job re-runs it, and for long batch work a re-run should be the author’s explicit choice.To opt in, add the
force-replacetrait (no properties) to the component. It sets theForceReplacedelivery intent on the component’s application (go-kure/launcher#782). kure’s Flux workflow turns that intokustomize.toolkit.fluxcd.io/force: enabledon every object the component emits — the Job — which kustomize-controller reads as its applyForceSelector: on an immutable-field error it deletes and recreates the object, so an update re-runs the Job, stopping any run in progress. The workflow annotates the Job once it is generated, so the annotation is not lost whencreateJobclears the generated Job’s annotations wholesale (job.Annotations = nil— a no-op since go-kure/launcher#361, because kure’sCreate<Kind>constructors now return TypeMeta and identity only; the assignment is kept so the field stays empty whatever a future constructor does). Annotating the component itself still does not reach the Job: a component’s own annotations, the ones beside itstypeandproperties, are read only for the tier annotation. Theannotationsproperty is another matter: it goes on the Job’s own metadata, as on every kind component (“The object’s labels and annotations” below). The Flux force annotation written there is on the Job as authored, and kustomize-controller reads it as it reads the workflow’s; the trait is the way that names no delivery engine. See the Trait Handlers catalogue.Outside the package,
spec.force: trueon the enclosing Flux Kustomization has the same effect for every resource it reconciles, and deleting the Job before redelivering avoids the error without either.cronjobdoes not have this problem: a CronJob’sjobTemplateis mutable, and each run creates a fresh Job.The generated
Jobcarriesapp: <component>as its own labels and its pod template’s, carries no annotations of the handler’s, and leavesspec.selectorunset for the job controller to fill. What thelabelsandannotationsproperties author is added to the Job’s own metadata, not to its pod template. The empty selector is deliberate and is the one workload kind where it stays that way: the Job controller generatesspec.selectorplus its matchingcontroller-uid/job-namepod labels server-side, so writing one here would fight it. Deployment, StatefulSet and DaemonSet get no such server-side defaulting and so are written explicitly — see Conventions.helm (go-kure/launcher#349,
helm.go) — the role-named Helm component: a component-position lowering rule (HelmRule), not a handler, that lowers to the kind-named terminals. Properties:chart,version,delivery(fluxdefault |template),source(inlineurlwith optionalkindand, for a GitRepository,ref; an inline Bucket’skind,endpoint,bucketName,provider,region,prefix; beside any of these anamefor the generated source; or a reference{name, kind, namespace}to an existing HelmRepository, GitRepository, Bucket, OCIRepository or HelmChart),values,valuesMode(inline|configMap),valuesConfigMapName,secretValues(the sensitive part of the values tree, see below),valuesSecretName,helmReleaseName(the name of the HelmRelease object, see below),scopeOverrides,hookGroupNamePrefixandlayoutKustomizationName(each underdelivery: templateonly, see below), and the HelmRelease keysinterval,releaseName,targetNamespace,driftDetection,install,upgrade,valuesFrom.delivery: fluxemits ahelmreleaseunder the authored name, with the authored traits and annotations. A HelmRepository, GitRepository or Bucket source (go-kure/launcher#336) becomeschart.spec.sourceRefwithchartrequired: the chart name, or for a GitRepository or Bucket the chart’s path in the fetched artifact. For a GitRepository or Bucket the rule also setschart.spec.reconcileStrategy: Revision, so a new source revision deploys even when the chart’s version is unchanged (Flux’sChartVersiondefault would skip it); a HelmRepository keeps that default. An OCIRepository or HelmChart source becomeschartRef.valuesand the HelmRelease keys are forwarded verbatim, so their shape is thehelmreleaseterminal’s to check.valuesModeis never forwarded, and the rule has no registration-time default for it:inline(or novaluesMode) keepsvalueson thehelmrelease.valuesMode: configMap(go-kure/launcher#702) with non-emptyvaluesmoves them into aconfigmaptrait the rule appends to thehelmrelease, after its authored traits, and dropsvaluesfrom it. The trait’s ConfigMap carries the values under the keyvalues.json, and avaluesFromentry{kind: ConfigMap, name: <it>, valuesKey: values.json}is placed before the authored entries, so an authored entry wins on a shared key (Flux mergesspec.valuesFromin list order). The values are serialized once, as indented JSON with sorted keys and numbers kept exact (JSON is YAML, which is how Flux reads a values reference); those exact bytes are both stored and hashed into the name:<component>-values-<first 10 hex digits of their sha256>. A name that would exceed 253 bytes is shortened by the one shortening rule (oam.ShortenNameWithSuffix, go-kure/launcher#793): the component name is cut to a prefix plus the first 10 hex digits of the sha256 of the full component name (8 before go-kure/launcher#793, so such a name changes once), and the suffix is kept, so it is always a legal DNS-1123 subdomain and always carries the values hash. Identical values hash alike whatever their key order, and any change to them renames the ConfigMap and so changes the HelmRelease’s spec, which is what makes Flux upgrade the release at once on a values-only edit (under this default name; seevaluesConfigMapNamebelow). Empty or absentvaluesadd no trait and no entry;valuesthat are not a JSON object, a non-finite number in them, or avaluesFromthat is not a list arehelm:build errors. Being an ordinaryconfigmaptrait, it is checked by that trait (a key it refuses, or data over the ConfigMap size limit, fails the build), labelledapp: <label value>like the trait’s other ConfigMaps, emitted after the HelmRelease, and moved with the release into the Flux namespace, since itsvaluesFromnames it. The trait builds its ConfigMap through theconfigmapkind’s own code (go-kure/launcher#741); the values travel as one string, so the kind’s string-only typing never refuses them. Known gap, shared with authored traits (go-kure/launcher#757): an authoredconfigmaptrait on the same component that takes the same name is not refused, and both ConfigMaps are emitted.valuesConfigMapNameandvaluesSecretName(go-kure/launcher#787) name the values ConfigMap and the values Secret (secretValues, below). Each name is resolved in the one order of every generated name: the author’s property, else the consumer’sNaminghook (rolesvalues-configmapandvalues-secret, asked with the component and the hashed default), else the default<component>-values-<hash>or<component>-secret-values-<hash>. A name from the author or the hook is used as written and gets no hash. It must be a DNS-1123 subdomain of at most 253 characters, and is refused otherwise, never shortened. Both objects follow the HelmRelease to the Flux namespace, so the name is held there against any other ConfigMap or Secret launcher names: one of the same name in the application namespace is another object.The cost of a fixed name. The default name moves with the content, so a values-only edit changes the HelmRelease’s
spec.valuesFrom, and Flux upgrades the release as soon as it sees the new HelmRelease. Under a name that does not move, the edit changes only the ConfigMap’s or Secret’s content, and the HelmRelease is unchanged. helm-controller still merges the referenced values on every reconciliation and upgrades the release when the result differs from the last one applied, so the edit is picked up at the release’s next reconciliation: within itsinterval, not at once. Flux reconciles at once only where the ConfigMap or Secret carries the labelreconcile.fluxcd.io/watch: Enabled, or helm-controller runs with a--watch-configs-label-selectorthat selects it. The rule sets no such label.A property that names nothing is refused:
valuesConfigMapNamewithoutvaluesMode: configMapor with emptyvalues,valuesSecretNamewith empty or absentsecretValues, and either underdelivery: template, which generates neither object. Two components of one document cannot write the same name for the same kind: each generates its own object, and the second is refused (helm: naming the values ConfigMap: name collision: ConfigMap "shared" is named by component "a" (role "values-configmap", set by valuesConfigMapName) and by component "b" (…); give one of them another name).helmReleaseName(go-kure/launcher#787) names the HelmRelease object, in the same order: the author’s property, else the consumer’sNaminghook (rolehelm-release, asked with the component), else the component name, so a document that does not set it and a hook that declines give the same output as before. A name that is not the default is used as written: a DNS-1123 subdomain of at most 253 characters, refused otherwise and never shortened.helmReleaseNameis the name of the HelmRelease object in the cluster (metadata.name);releaseNameis the name Helm gives the release (spec.releaseName, the chart’s.Release.Name). Renaming the object moves nothing else:spec.releaseNamestays the authoredreleaseNameor the component name, so the installed release is not reinstalled under another name, the values ConfigMap and Secret keep their names, and thehelmreleasemember keeps the component’s name, with its group, its placement and its traits. Nothing launcher writes names the HelmRelease, so no reference moves. The name is claimed where the HelmRelease lands, the default included: an authoredhelmreleasecomponent whoseobjectNameis the same name is refused in the transform with both named (name collision: HelmRelease.helm.toolkit.fluxcd.io "default/chart" is named by component "chart" (role "helm-release", its default) and by component "rel" (role "object", set by properties.objectName); give one of them another name). Underdelivery: templatethe property is refused, since no HelmRelease is generated, and the hook is not asked.secretValues(go-kure/launcher#786) is a second values tree, for the values that must not sit in the HelmRelease or in a ConfigMap. It is an object likevalues; absent,nullor empty it changes nothing. Thehelmreleasekind has no such property: there, name a Secret invaluesFrom.- Under
delivery: fluxthe rule appends asecrettrait to thehelmrelease, after its authored traits and after the values ConfigMap’s trait. The trait’s Secret holds the tree under the keyvalues.json, serialized as the values ConfigMap’s is, and is named<component>-secret-values-<first 10 hex digits of the sha256 of those bytes>, shortened past 253 bytes by the same rule (oam.ShortenNameWithSuffix). AvaluesFromentry{kind: Secret, name: <it>, valuesKey: values.json}is placed after the values ConfigMap’s entry and before the authored entries, so the order is: values ConfigMap, values Secret, authored. No value ofsecretValuesis written to the HelmRelease or to the values ConfigMap. Being an ordinarysecrettrait, it is checked by that trait (the 1 MiB limit), labelledapp: <label value>, emitted after the HelmRelease, and moved with the release into the Flux namespace. A change insecretValuesrenames the Secret and so changes the HelmRelease, which is what makes Flux upgrade the release at once (under this default name; seevaluesSecretNameabove). - Under
delivery: templatethe tree is passed to thehelmtemplate, which merges it overvaluesfor the render (see helmtemplate). Nothing is emitted for it: a value is in the output only where the chart renders it. - A path set in both
valuesandsecretValuesis refused, under either delivery and either values mode:helm: auth.password is set in both values and secretValues; a path may be set in only one of them. Two objects at the same key are compared key by key; anything else at a key both trees set (a scalar, a list, a null, and a nil map a Go caller passes, which is a null) is a shared path. Underdelivery: templatethe two trees are compared as they are, without a JSON round trip, since the render takes the values with their Go types: an object is amap[string]any, which is all a parsed document holds, and a map of another type a Go caller passes (map[string]string) at a key both trees set is a shared path too. An empty key is a key like any other and is written""in the message. Without the refusal the winner would depend on the values mode, since Flux applies inlinespec.valuesafter everyvaluesFromentry. - A key named
globalbelow the top level ofsecretValuesis refused, under either delivery and either values mode, before anything is rendered or emitted (go-kure/launcher#794, item 9):helm: secretValues: redis.global: a key named global is allowed only at the top level, …. Helm reads<dependency>.globalas that dependency’s globals. Where their shape conflicts with the chart’s ownglobalat a key, a table on one side and a plain value on the other, it drops the dependency’s entry and prints it in a warning that cannot be intercepted: in the build’s log underdelivery: template, in the Flux controller’s underdelivery: flux. Launcher does not read the chart, so it cannot tell a dependency from any other key: a key namedglobalbelow the top level that is not a dependency’s globals cannot be given throughsecretValueseither, only throughvalues. Put a sensitive global under the top-levelglobal, which every dependency receives and Helm does not print. The tree is searched as JSON, through objects only: a key in a list element is not refused. The message names the path by its keys and no value. - Policy. A policy that forbids explicit secrets
(
oam.ExplicitSecretPolicy, see thepkg/oamREADME) refuses a component withsecretValues, under either delivery, with a violation naming the component. A policy that does not implement that optional interface allows it. - No refusal of the property repeats a value. It names
secretValues, a path by its keys, or the type of a wrong value, and never wraps an encoding or decoding error, which could quote one. Underdelivery: templatean error about an object the chart rendered is another matter (see helmtemplate, “not covered”).
Security note.
secretValueskeeps sensitive values out of the HelmRelease and the values ConfigMap. It does not encrypt them. The generated Secret is in the build output in clear form (datais base64, an encoding): the output is exactly as sensitive as the input document, and so is every place it is written to, a Git repository or an OCI artifact included. Encrypting it is the consumer’s business (SOPS or the like, on the written manifests). The Secret’s name, and the HelmRelease naming it, carry 40 bits of a digest of the tree, so whoever reads either can test a guess of the whole tree against it. To keep a value out of the document and the output altogether, create the Secret out of band (anexternal-secrettrait, a sealed or externally managed Secret) and name it invaluesFrom, or use the chart’s own existing-secret values.- Under
An inline source also emits the source: a
helmrepositorywith only the URL forhttp(s)://, or anocirepositorywithref.tag: <version>foroci://. A Git repository needskind: GitRepositoryset (anhttp(s)://URL alone means a Helm repository) and emits agitrepositorywith the URL (http://orhttps://) andsource.ref, which must set exactly one ofbranch,tag,semver,name,commit: Flux would otherwise check out branchmaster, or pick one of several fields by precedence. The schema publishes the rule as anexclusivegroup onsource.ref, and a field authored as an empty string beside a set one counts as a second field.kind: BucketwithendpointandbucketName(and optionallyprovider,region,prefix), and nourl, emits abucketwith exactly those keys. Credentials (secretRefand the like) have no inline form: author thegitrepositoryorbucketcomponent and reference it. That includes everyssh://repository, which Flux reads only with a key fromsecretRef. Theocirepositoryalso selects the Helm chart content layer (application/vnd.cncf.helm.chart.content.v1.tar+gzip) withoperation: copy. Flux therefore passes the chart archive through unchanged. By default it would extract and re-archive the chart, dropping files that its ignore rules exclude (*.zip,*.png, …) even when the chart reads them with.Files.Get. It is named<document>-source-<digest>, where the 10-hex digest is taken over the content identity:helm:<url>,oci:<url>:<version>,git:<JSON of url and ref>, orbucket:<JSON of provider, endpoint, bucketName, region, prefix>(JSON, so a:inside a URL or endpoint cannot make two identities collide). Components of one document with the same identity share one source; the first emits it and the rest only reference it (LoweringContext.ResolveSharedName). Different documents never share or collide, because the document name is part of the source name. The source carries no traits and keeps its terminal’s interval default rather than the releaseinterval. The source terminal’s registry allowlist (ApplyPolicy) applies to it.Naming the generated source (go-kure/launcher#787). The name is resolved in the one order of every generated name: the author’s
source.name, else the consumer’sNaminghook, else the default above.- The hook is asked under role
helm-source, once for each source identity of a document, with the default<document>-source-<digest>and no component, since the source belongs to the document: every component of that identity that names no source adopts the answer. source.namebeside an inline source names the source that component generates. The name is the component’s own choice. Components that write the same name for the same identity share one source; a component that writes none does not share it, and gets the document’s source beside it (two sources of one identity), unless the name written is that source’s own (its default, or the hook’s answer): one name for one identity is one source. One name for two identities, or for two kinds, is refused as a collision naming both components, and so is a hook answer that gives two identities one name.- A name from the author or the hook is used as written: it must be a DNS-1123 subdomain of at most 253 characters and is refused otherwise, never shortened.
- The source cannot take the name of a component. It is a component of the
lowered document itself, so
source.nameequal to the component’s own name, or to the name of another component the document holds, is refused (helm: source.name "web" is the component's own name; the generated source is a component of the document too, so give it another name). The default and a hook answer are held to the same rule (helm: the generated source would be named "shop-source-…", the name of a helmrepository component of the document; … so rename that component, name the source with source.name, or have the Naming hook return another name for role "helm-source"). To give a HelmRepository and its HelmRelease one name, author thehelmrepositorycomponent withobjectNameand reference it. - The source lands beside the HelmRelease, in the Flux namespace when one is set. Its name is held there against every other object of its kind launcher names; an object of the same name and kind in the application namespace is another object.
source.namealone, with no inline source, references an existing source as before, andsource.namespacebelongs to that form only.
Breaking (go-kure/launcher#787). A
helmcomponent with bothsource.urlandsource.namewas refused and builds now, underdelivery: flux. The refusal ofsource.namespacebeside an inline source readshelm: source.namespace is only valid with a reference to an existing source (source.name and no inline source); a generated source is created beside the HelmRelease. An authored component that already has the name a generated source would take by default was refused bypkg/oamas a duplicate component name, and is now refused by the rule with the message above.- The hook is asked under role
The generated source is applied with the application bundle, ahead of every group (go-kure/launcher#783). The rule orders the release after the source it generates or adopts (
Component.OrderAfter), andpkg/oamputs such a source among the application bundle’s own applications, beside the ordered groups. A source several releases share therefore exists once and precedes all of them, whatever tier a release is placed in. Aplacementpolicy or tier annotation naming the generated source, and adependencyrule making it wait on a component, fail the build. A source the component references bysource.nameis the author’s own component and the rule orders nothing after it.delivery: templateemits ahelmtemplatewith the URL, its resolved kind,chart,version,values,secretValues,scopeOverridesand an authoredreleaseName, which thehelmtemplatechecks and, when unset, defaults to the component name, the release name thedelivery: fluxHelmRelease carries too (go-kure/launcher#785; see helmrelease). An authoredhookGroupNamePrefix(go-kure/launcher#787) is passed on as written and is thehelmtemplate’s; underdelivery: fluxit is refused with ahelm:message, since a HelmRelease installs the chart, no hook-group layout exists and the prefix would name nothing. An authoredlayoutKustomizationName(go-kure/launcher#787) is passed on and refused the same way: underdelivery: fluxthe component has no layout of its own. No source is emitted, and an authoredvaluesMode: inlineis dropped. The rule refuses everything a client-side render cannot honour, each with ahelm:message:- a source reference, and
valuesMode: configMap; source.namebeside an inline source,valuesConfigMapName,valuesSecretNameandhelmReleaseName(go-kure/launcher#787): the render generates no source, no values ConfigMap, no values Secret and no HelmRelease, so each would name nothing;- an inline GitRepository or Bucket source, since the render fetches only from a Helm or OCI repository;
- an OCI source without
version; - every other HelmRelease key (
interval,targetNamespace,driftDetection,install,upgrade,valuesFrom), whichhelmtemplatedoes not accept.
- a source reference, and
scopeOverrides(go-kure/launcher#794, item 11) states the scope of a kind the chart renders, fordelivery: template: the list of{apiVersion, kind, scope}thehelmtemplateandmanifestscomponents take, with the meaning it has there (see Scope overrides under helmtemplate). The rule forwards the authored list to thehelmtemplateas written, under the declared spelling of the key, so a chart delivered throughhelmand one authored as ahelmtemplatewith the same list render the same objects.nullreads as omission under either delivery.- A malformed entry is refused by the rule, with the shared parser’s message
under its own prefix (
helm: scopeOverrides[0]: scope "cluster" is invalid; must be "Cluster" or "Namespaced"), so the message names the component type the author wrote. An entry that contradicts aCustomResourceDefinitionthe chart renders is refused when the chart is rendered, by thehelmtemplate. delivery: fluxrefuses the property, set or defaulted and whatever its shape:helm: delivery: flux does not support scopeOverrides (only a client-side render reads it). Helm creates a HelmRelease’s objects in the cluster, so nothing in the build could apply a stated scope, and dropping the property would let an author believe it took effect.
- A malformed entry is refused by the rule, with the shared parser’s message
under its own prefix (
Strict, unlike the removed
helmchartcomposite. An undeclared key at the top level or insidesourceis refused, and so are:delivery: native;source.namespacewith an inline source;- an inline HelmRepository or OCIRepository URL carrying a user or password
(
https://user:token@host,oci://user@registry/chart), under either delivery: the URL is copied verbatim into the generated source or the render, so the credential would sit in plain text in the output. Credentials go in thesecretRefof an authoredhelmrepositoryorocirepository, referenced withsource.name; a client-side render takes none. No message repeats the URL, not even the refusal of one that does not parse; - an inline GitRepository without exactly one
source.reffield, or with a URL that is nothttp://orhttps://(anssh://one needs credentials), or one that is more or less than a host, an optional port and a repository path (user info, a query or a fragment is refused), or whose port is outside 1–65535; urlwith an inline Bucket, or one withoutendpointorbucketName, or anendpointthat is more than ahost[:port]or anhttps://URL of a host and port (user info, a path, a query, a fragment orhttp://is refused: a non-TLS endpoint needsinsecure: true, which only an authoredbuckettakes) or whose port is outside 1–65535, or aproviderother thangeneric,aws,gcporazure(checked at the helm rule; thebucketterminal leaves it to the CRD);source.refother than on an inline GitRepository, andendpoint,bucketName,provider,region,prefixother than on an inline Bucket;chartwith an OCIRepository or HelmChart source;versionwith a referenced OCIRepository or HelmChart source, which pins its own;versionwith a GitRepository or Bucket source: Flux reads that chart at the source’s fetched revision and ignoresversion;- two keys equal ignoring case, at the top level or inside
source(chartandChart): the decode would match both to one field and keep either.
An authored component already named like a generated source fails the build: the rule refuses it when the document holds that component as the rule runs (see Naming the generated source), and
pkg/oamrefuses it as a duplicate component name when another rule emits it in the same lowering round.helmchart — since go-kure/launcher#351, the kind-named terminal for Flux’s
HelmChart: see helmrepository / ocirepository / gitrepository / bucket / helmchart. Until go-kure/launcher#350 the name belonged to a role-level composite (a HelmRelease plus its source, or a client-side render), which is gone; usehelm. A document written for the composite fails validation on a keyHelmChartSpecdoes not declare. When that key is one of the composite’s own thatHelmChartSpeclacks (delivery,releaseName,targetNamespace,source,values,valuesMode,driftDetection,install,upgrade,valuesFrom), the error adds a pointer tohelm; any other unknown key gets the plain error. To migrate, changetype: helmcharttotype: helmand renamedelivery: nativetodelivery: flux; the other properties keep their names. The output then changes only as this table says.TestHelmParity(pkg/cmd/kurel) pins it on three fixture pairs: the lasthelmchartoutput of each input, frozen before the removal, is diffed against thehelmbuild, and the diffs are inpkg/cmd/kurel/testdata/helm-parity/:# What changes with helm1 The values ConfigMap name carries a values hash. 2 A generated source is named <app>-source-<digest>, not after the component.3 (void) ociand Helm-over-OCI components: neither shares an OCIRepository with the other (go-kure/launcher#665).4 The delivery value nativeisflux.5 (void) targetNamespaceunder a Flux namespace: both default it to the application namespace (go-kure/launcher#625).6 A generated source keeps its terminal’s default interval, not the release interval. 7 No registration-time valuesModedefault;valuesModeis never forwarded (underconfigMapthe values become aconfigmaptrait).8 (void) Generated sources: neither gets an automatic health check; launcher sets none (go-kure/launcher#781). 9 The registry allowlist applies to inline sources. 10 Generated sources are applied with the application bundle, ahead of every group. A tier annotation on the component places only its release. 11 Template delivery refuses targetNamespace, and an unsetreleaseNamerenders under the component name, shortened as Flux shortens a release name, instead ofrelease(go-kure/launcher#776, go-kure/launcher#785).12 chartis refused with an OCIRepository or HelmChart source.13 versionis refused with a referenced OCIRepository or HelmChart source.14 source.namespaceis refused together withurl.15 Unknown keys are refused at any depth of source, and so are two keys that differ only in case.16 (void) A generated OCIRepository: both set layerSelector(the chart content layer,copy) (go-kure/launcher#665).17 placementmay not place a generated source in any tier, and adependencyrule may not make it wait.18 Any other helmchartdefault or build-time check a terminal does not reproduce (strict decoding).19 The HelmRelease carries the component label and a post-renderer that sets it on the chart’s pod templates, as every component’s output does (go-kure/launcher#788). 20 Every HelmRelease carries spec.releaseName: the authored value, else the component name, wherehelmchartleft an unset one to Flux (go-kure/launcher#785).Two
helmchartbehaviours have nohelmcounterpart beyond row 18:valuesMode: configMapno longer makes the component aLayoutAugmenter(the values ConfigMap comes from aconfigmaptrait on thehelmrelease, ordinary build output, sokurel buildaccepts it and a layout-walking consumer no longer moves the component into a sub-layout), and underdelivery: templatethe render’s.Release.Namespacecan no longer be authored (row 11; see helmtemplate).helmrelease — the kind-named terminal for Flux’s
HelmRelease(go-kure/launcher#327, part of the Helm-family redesign go-kure/launcher#336). Its properties are exactly the top-level JSON keys ofHelmReleaseSpecin the helm-controller API versiongo.modlinks; a test ties the published schema to that struct, so a helm-controller bump that adds or drops a spec field fails the suite until the schema follows. It creates no source:chart.spec.sourceReforchartRefnames one that already exists.Decoding. The whole property map is decoded with
builtin.DecodeStrictJSONintoHelmReleaseSpec. A key the struct does not declare, at any depth (chart.spec.chartVersion, a stray key inside avaluesFromentry), is refused by name, and so is a wrongly typed value (suspend: "yes",maxHistory: "3", an unparsable duration). The schema keeps the nested Flux blocks as open objects; the strict decode is what checks them.valuesModeis refused like any other unknown key; the error adds a pointer to thehelmcomponent’svaluesMode: configMap, or aconfigmaptrait plus avaluesFromentry, which replace it (go-kure/launcher#702). Known gap, inherited from the decoder: inside a type with its ownUnmarshalJSONunknown keys are not refused —valuesis the case that matters, and it is open by design. Keys match case-insensitively, as inencoding/json; schema validation, which akurel buildruns first, is exact.Defaults and checks.
intervaldefaults to60mwhen unset (a zero duration counts as unset); Flux requires the field. A setintervalmust be a duration Flux accepts (go-kure/launcher#590, go-kure/launcher#601;ociapplies the same check). The CRDs require^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$: unsigned, inms,s,morh(10m,1h30m,1.5h).time.ParseDurationalone would also accept-5mor500us, which would fail at apply time; both are build errors. The value is emitted as ametav1.Duration, which serializesDuration.String()rather than the authored text, so that form is checked too:0.5msmatches the pattern but is written as500µs, and is refused as below Flux’s millisecond resolution. A positive value too small for the duration type (0.0000000001ms) is refused the same way rather than emitted as0s. In practice any value of0sor at least1msis accepted. The check runs on the authored text, which also refuses a positive value that would decode to zero and be replaced by the default, and again inGenerateon the decoded duration’s emitted form, for a config built directly. It lives in the internalpkg/oam/internal/fluxduration. Its other duration fields are checked the same way, in the same form (go-kure/launcher#606):timeout,chart.spec.interval,install.timeout,upgrade.timeout,test.timeout,rollback.timeout,uninstall.timeout, andinstall.strategy.retryIntervalandupgrade.strategy.retryInterval. A nested key matches case-insensitively at every level, as the decode does. Exactly one ofchartandchartRefis required. The published schema lists both as optional and says so in the handler’sexclusivegroup (go-kure/launcher#790): a validator built from it refuses both and neither, as the build does. Under an authoredchart,chart.spec.sourceRef.kindis required (go-kure/launcher#790): the API requires it and takesHelmRepository,GitRepositoryorBucket, the Go type leaves an empty one out of the object, and the kind does not choose a source’s kind for the author, so a reference without one is refused withchart.spec.sourceRef.kind: required (…). Which of the three it is stays the CRD’s to check. An authoredchart.spec.verifythat names noprovideris written withprovider: cosign, the API’s default: the Go type writes the field whether or not it was authored, so the API’s default never applies, and the object is then what the API would have made of the omitted field. An authoredprovider: ""is refused, since the type cannot tell it from an unauthored one and the API’s enum (cosign,notation) refuses it as written. Two spellings of one key on the way (Providerandprovider) are refused, since the decode keeps one value and drops the other.TestKindComponents_OmittedRequiredAndWrittenDefaultsholds both to the markers in the source of the linked Flux modules, which ship no CRD: the required field to a refusal shown on a document, and the written value to the default the marker states, so an upstream change of that default fails the test. “Required” there is what the markers say; no API server was asked.valuesmust be a JSON object; a non-finite number (.nan,.inf) is a build error, never a panic. EachvaluesFromentry is held to theValuesReferenceCRD’s own constraints, and a violation is a build error naming the entry’s index:kindisSecretorConfigMap, exactly as Flux’s enum spells them (go-kure/launcher#748);nameis required and at most 253 characters; a setvaluesKeyis at most 253 characters and matches^[\-._a-zA-Z0-9]+$; a settargetPathis at most 250 characters and matches the CRD’s pattern (dot-path characters,\,/, and[n]indexes of up to five digits) (go-kure/launcher#762). Lengths count characters, as the CRD’smaxLengthdoes, not bytes. Like the other checks these run again inGenerate, and they cover thevaluesFromahelmcomponent passes through. Nothing else is checked here: the enum checks ondriftDetection.mode,install.crdsandupgrade.crdsare left to the HelmRelease CRD’s own admission, and so are the limits of an authoredreleaseName.Release name (go-kure/launcher#785). A component that authors no
releaseNamegetsspec.releaseNameset to the component name, so every HelmRelease in the output names its release. A name over 53 characters, Helm’s limit, is shortened as Flux helm-controller shortens a release name — its first 40 characters,-, and the first 12 hex digits of the SHA-256 of the whole name (oam.ShortenNameatoam.ShortenLimitHelmRelease, go-kure/launcher#793) — so the written name is the one Flux would shorten the component name to itself. An authoredreleaseNameis written as it is, never shortened.helmtemplaterenders under the same default, so ahelmcomponent releases its chart under one name withdelivery: flux, with or without a Flux namespace, and withdelivery: template. A default Helm would not install under is a build error with the remedy to setreleaseName: the shortening can cut a dotted name just after a., leaving a label that starts with-. The check runs again inGenerate, where aHelmReleaseConfigbuilt directly with neither aNamenor areleaseNameis refused as well: it has nothing to derive a default from.Breaking output change (go-kure/launcher#785): every HelmRelease gains
spec.releaseName. Left unset, Flux names a release<targetNamespace>-<name>whenspec.targetNamespaceis set and<name>otherwise, so the release name itself changes wherever a target namespace is set and noreleaseNameis authored: under a Flux namespace, which defaults one (below), and with an authoredtargetNamespacealone. There the releaseshop-webof a componentwebwith target namespaceshopbecomesweb. Flux does not rename a release that is already installed: it uninstalls the existing release and installs a new one under the new name (the warning on.spec.releaseNamein Flux’s HelmRelease reference). To keep the installed release, author its name asreleaseNameon such a component:<targetNamespace>-<name>, or, where that is over 53 characters, the shortened name Flux installed it under (its first 40 characters,-, 12 hex digits of its SHA-256; thenameof an entry in the HelmRelease’sstatus.history). The long form is not accepted there: the HelmRelease CRD limitsreleaseNameto 53 characters. Without a target namespace the name stays<name>and only the field is new.Identity and namespaces. The HelmRelease is named after the component and lands in the Flux namespace when one is configured, else in the application namespace (
SetFluxNamespace). Under a Flux namespace, a component that does not authortargetNamespacegetsspec.targetNamespaceset to the application namespace, so the release still installs there rather than into the Flux namespace; an authored value wins. The release’s Helm storage follows the release (go-kure/launcher#988): a component that does not authorstorageNamespacegetsspec.storageNamespaceset to the effective target namespace, authored or defaulted, so thesh.helm.release.v1.*Secrets, which hold the rendered manifests and the merged values, stay with the release rather than collect in the Flux namespace; an authored value wins. Without a Flux namespace neither is set, and helm-controller keeps the storage in the HelmRelease’s own namespace, the application namespace. Ahelmcomponent takes nostorageNamespace, so its release always gets the default. The Kustomizationociemits has no such default, deliberately (go-kure/launcher#794, item 1): a HelmRelease’stargetNamespaceonly says where the release installs, while a Kustomization’s overrides the namespace of every namespaced object in the artifact (seeoci). The ConfigMaps and Secrets the HelmRelease reads from its own namespace —valuesFrom,kubeConfig.secretRef/configMapRef, andchart.spec.verify.secretRefwhen helm-controller creates the HelmChart beside the release (chart.spec.sourceRefnames no namespace, or names the Flux namespace) — must live in the Flux namespace too. Aconfigmap,external-secretorcertificatetrait on the component whose object one of them names moves there with the release; one none of them names stays in the application namespace with the release’s workloads (go-kure/launcher#740). A trait ConfigMap named invaluesFromtherefore leaves the application namespace even when the chart’s pods read it too; give the pods their own copy, under another name, if they need it. The target namespace does not reach the release name:spec.releaseNameis always set (see Release name), so Flux does not derive<targetNamespace>-<name>. AuthorreleaseNamewhen a specific release name matters — for instance when taking over a release installed under another name.Breaking output change (go-kure/launcher#988): under a Flux namespace every HelmRelease that authors no
storageNamespacegainsspec.storageNamespace. Left unset, helm-controller kept the release’s Helm storage in the Flux namespace, where the HelmRelease sits. Where such a release is already installed, helm-controller reads the changed storage namespace as a changed release target: it uninstalls the existing release from the old storage namespace and installs it again in the new one (itsReleaseTargetChangedcheck), so the release’s objects are deleted and recreated and its history starts over. To keep an installed release in place, authorstorageNamespaceas the Flux namespace on ahelmreleasecomponent. Output without a Flux namespace is unchanged.Values in a ConfigMap. The HelmRelease is the only object
helmreleaseemits. Until go-kure/launcher#702 it took a launcher-ownedvaluesMode: configMapand emitted the values ConfigMap itself; that key is gone (see Decoding). Thehelmcomponent’svaluesMode: configMapproduces the same ConfigMap through aconfigmaptrait (see helm), and on ahelmreleaseauthored directly the same result is aconfigmaptrait plus avaluesFromentry naming it.helmtemplate — the kind-named terminal for a client-side Helm render (go-kure/launcher#348, part of the Helm-family redesign go-kure/launcher#336), and what the role-named
helmrule lowers to underdelivery: template. It fetches and renders the chart at build time and emits the rendered manifests, each checked against the environment policy (see Policy below). It creates no source CR and noHelmRelease.Properties.
sourceis required:url(required) is anhttp://orhttps://Helm repository URL, or anoci://URL naming the chart;kind(optional,HelmRepositoryorOCIRepository) is inferred from the scheme when unset —oci://isOCIRepository, anything elseHelmRepository— and must agree with it when set. Aurlcarrying a user or password is refused, since the render takes no credentials, and the message never repeats it.chartis required for aHelmRepository; anOCIRepository’s URL already names the chart, so therechartis not used.versionis required for anOCIRepository.valuesis an open object, the Helm values tree, and must be representable as JSON: a non-finite number (.nan,.inf) is a build error. The source checks are shared with thehelmrule’s inline source rather than copied.scopeOverridesstates the scope of a kind the chart renders (see Scope overrides below).hookGroupNamePrefix(go-kure/launcher#787) is the prefix of the names of the hook-group layouts, in place of<application>-<component>, and so of their Flux Kustomizations underFluxIntegratedPerLayout; so does a prefix theNaminghook returns for thehook-grouprole. With neither, the default prefix names only the layouts: kure names each Kustomization<unit>-<layout name>(go-kure/launcher#941). See the hook-group paragraph below.layoutKustomizationName(go-kure/launcher#787) is the name of the Flux Kustomization of the component’s own layout, in place of<bundle>-<component>; see the same paragraph.secretValues(go-kure/launcher#786) is a second open object, for the sensitive part of the values tree; it is what thehelmrule forwards its ownsecretValuesas. The chart is rendered withsecretValuesmerged overvalues, object by object, with the authored value types. The terminal emits nothing for it, so a value is in the output only where the chart renders it: a chart that puts it in a Secret emits that Secret in clear form, and one that puts it in a ConfigMap or an environment variable emits it there. The output is as sensitive as the input; see the security note under helm. Further:- a path set in both trees is refused, naming the path
(
helmtemplate: auth.password is set in both values and secretValues; …), by the rule thehelmcomponent states; - a key named
globalbelow the top level is refused, naming the path, before any fetch (helmtemplate: secretValues: redis.global: a key named global is allowed only at the top level, …), by the rule thehelmcomponent states and with its trade-off: such a key that is not a dependency’s globals can be given throughvaluesonly, and a sensitive global goes under the top-levelglobal; - a policy that forbids explicit secrets (
oam.ExplicitSecretPolicy) refuses the component inApplyPolicy, before any fetch (helmtemplate: secretValues is set and the environment policy forbids explicit secrets; …). Not covered: a Secret the chart itself renders. It is emitted under such a policy, where thepassthroughandmanifestscomponents refuse a Secret they carry: its content comes from the chart and its values, and most charts render one; - a render or decode failure is not reported as Helm reports it, since a template error can
quote a value (
fail,required, a YAML parse error showing the line). The chart is rendered a second time withvaluesalone. If that fails too, its error is the one reported, afterrendering chart with secretValues failed; the cause is withheld because it can repeat a sensitive value. Without them it fails with: …. If it succeeds, the message is onlyrendering chart with secretValues failed, and without them it renders; the cause is withheld because it can repeat a sensitive value. To debug such a chart, render it with placeholder values invalues. The second render repeats the fetch; - not covered: an error about a rendered object. No check that runs on what the chart
rendered, once it has decoded, scrubs its error. It names the object it refuses by kind
and name, and can quote any part of the object it refuses or locates the refusal by. The
policy checks (see Policy) quote a container’s or a volume’s name, an image reference,
a resource quantity, a capability; the transform’s component label quotes the key of a
label that is not a string. So any part of a rendered object can reach an error: a chart
that builds one from a sensitive value has it quoted there. The refusal of a field the kind’s type does not
declare is not among them: it is a decode failure, withheld as above. The refusal of a
scopeOverridesentry that contradicts a CustomResourceDefinition the chart renders (see Scope overrides below) is such an error and is not withheld: it names the kind, the object’s name and the two scopes, never a value; - Helm’s own warnings about a values conflict quote the value Helm drops. With the refusal
above that is never a value of
secretValues: for a top-levelglobalit is the dependency’sglobalentry fromvalues, and for a dependency’s ordinary key it is the dependency’s default. A test pins both, each way round, on the process’s standard log and its defaultsloglogger.
Release name.
releaseNameis the render’s.Release.Name. Unset, it is the default thehelmreleaseterminal writes tospec.releaseName(go-kure/launcher#785): the component name, and for a name over 53 characters, Flux helm-controller’s shortened form of it — its first 40 characters,-, and the first 12 hex digits of the SHA-256 of the whole name. Ahelmcomponent therefore renders underdelivery: templatewith the same release name its HelmRelease is installed under withdelivery: flux, whatever its target namespace. The chart and its source play no part. The name, authored or defaulted, must be a valid Helm release name by Helm’s own rule — a DNS-1123 subdomain of at most 53 characters — since kure’s render checks nothing. An authored name is never shortened: one over 53 characters is refused. A default that is not valid — the shortening can cut a dotted name just after a., leaving a label that starts with-— is refused with the remedy to setreleaseName, as Flux would fail to install it. AHelmTemplateConfigbuilt directly defaults it fromNamethe same way, and is refused when both are empty. Since component names are unique in a document, two renders of one chart no longer share a release name by default. Two given the samereleaseNamein one namespace generate the same objects when the chart names its objects after the release, as most do, andkurel buildrefuses them as a generated-object collision rather than merging them. Before go-kure/launcher#776 every render used kure’s defaultrelease; the project is pre-GA, so the changed object names of an existing template-rendered chart carry no compatibility shim.Decoding.
values,secretValuesandscopeOverridesare split off, and the rest of the property map is decoded withbuiltin.DecodeStrictJSONinto a closed struct, so any other key, at any depth, is refused by name, as is a wrongly typed value.valuesitself reaches the render exactly as authored, with its YAML-decoded value types, rather than the strict decoder’sjson.Numberre-reading, which a chart template comparing a value with a number would treat differently;secretValueslikewise. AsecretValuesthat is not an object is refused by its type, never by its content. The schema declares the same keys, withsourceclosed tourlandkind, and a test ties it to the struct. Keys match case-insensitively in the handler, as inencoding/json; schema validation, which akurel buildruns first, is exact.Refused outright.
targetNamespace, every property only a Flux-reconciled release reads (interval,driftDetection,install,upgrade,valuesFrom,valuesMode), thehelmrule’sdeliveryswitch, and a source reference (source.name,source.namespace) are undeclared keys: schema validation refuses each, and so does the strict decode. AnOCIRepositorysource withoutversionis refused by the handler. The key itself is what is refused, whatever it holds. The render’s.Release.Namespaceis the application namespace; the terminal declares notargetNamespace, so it cannot be set.Namespace. A namespaced rendered object that carries no
metadata.namespaceis given the application namespace, where a Helm install into that namespace would create it (go-kure/launcher#794, item 4). Each object’s scope is resolved as themanifestscomponent resolves it, by the one function both call (resolveObjectScope,manifests.go): ascopeOverridesentry for the object’s kind, else kure’s scope table (manifest.Scope) — the kinds kure registers, in any API version — plus the scope aCustomResourceDefinitionamong the emitted objects declares for the kind it defines. Unlikemanifests, the render refuses nothing about what the chart wrote, since the application does not author a chart:- a namespace the chart wrote is kept, on a cluster-scoped object too, as Helm keeps it;
- a cluster-scoped object without one stays without;
- an object of unknown scope without one is left as rendered, with no namespace and no error:
a kind kure does not register — a custom resource, or a built-in of an API group kure’s
scheme does not hold — with no CRD for it among the emitted objects and no
scopeOverridesentry.Lease,EndpointSliceand the image-reflector kinds (ImageRepository,ImagePolicy) are registered, so one without a namespace is given the application namespace. Whoever applies the output decides where it lands (Flux’stargetNamespace, a client’s default namespace). A chart’scrds/directory is not rendered, and a CRD under a dropped hook is not emitted, so neither gives a kind a scope.
The policy check runs on the stamped objects, so a violation names an object with the namespace it is emitted in. A
HelmTemplateConfigbuilt directly with an emptyNamespacehas none to give and stamps nothing.Scope overrides (go-kure/launcher#794, item 11).
scopeOverridesstates the scope of a kind the chart renders, for a kind of unknown scope above: a list of{apiVersion, kind, scope},scopebeingClusterorNamespaced, matched on the exactapiVersionandkind. It is the propertymanifestshas, read by the same parser (parseScopeOverrides) and resolved by the same function, so an entry means the same on both: it outranks kure’s own table, is ignored for a kind the Kubernetes API itself scopes, and must agree with aCustomResourceDefinitionamong the emitted objects (see crd / manifests). A test holds the two components to the same answer for the same objects and overrides.Namespaced: an object of the kind withoutmetadata.namespaceis given the application namespace, as a kind kure knows to be namespaced is.Cluster: an object of the kind is left as rendered. That includes one the chart wrote a namespace on, whichmanifestsrefuses: template delivery refuses nothing about what a chart wrote.- An entry for a kind the chart does not render does nothing.
nullreads as omission.
Refused, two cases only. A malformed entry, at decode and before any render, with the
manifestsmessages under this component’s prefix (helmtemplate: scopeOverrides[0]: scope "cluster" is invalid; must be "Cluster" or "Namespaced"). And an entry that contradicts aCustomResourceDefinitionthe chart renders for the kind, when the chart is rendered (helmtemplate "<component>": object Widget "w": scopeOverrides says Cluster but the CustomResourceDefinition for Widget.fixtures.example.com the chart renders declares Namespaced; …): that CRD defines the scope the cluster will serve, so no cluster can honour the entry. The message names the object’s kind and name and the two scopes, nothing else of the object. AHelmTemplateConfigbuilt directly holds the overrides inScopeOverrides, where a value other than the two scopes is refused before the render.The
helmrule takes the same property underdelivery: templateand forwards it here as written, after refusing a malformed entry under its own prefix; underdelivery: fluxit refuses the property (see helm).Not covered: no policy or placement check on template output reads an object’s scope, so the overrides change the namespace stamp and nothing else. Not breaking: a document that does not use the property renders as before.
Breaking output change (go-kure/launcher#794): such objects gain
metadata.namespacein the output. Before, a chart that left it unset rendered namespace-less objects, which landed wherever the applying client defaulted them.Breaking output change (go-kure/launcher#790, with the kure version that registers them):
Lease,EndpointSlice,PriorityClass,RuntimeClass,APIService, theadmissionregistration.k8s.io/v1kinds and the image-reflector kinds are read as their Go types, no longer as kinds the build does not know. One that sets only fields its type declares is emitted from that type; one that sets a field the type does not declare is emitted as rendered, the field kept (Undeclared fields, below); a field of the wrong type is now a build error. The namespaced ones (Lease,EndpointSlice,ImageRepository,ImagePolicy) gainmetadata.namespacewhen the chart left it unset. A chart that renders av1Listor a typed list now builds, and each of its items is held to the policy and to Undeclared fields as a document of its own is. A list of a kind the scheme does not register no longer builds when ahelm.sh/hookannotation is involved, on the list or on an item (see “Rendered objects”): it built before, with a wrong output, the list’s hook lost and its items emitted as ordinary resources.Breaking (go-kure/launcher#790, with the kure version that reads a list in one place, go-kure/kure#1014), for template delivery,
manifestsandcrdalike. Built before, refused now:Document Before Now A list (a v1List, a typed list, a list of an unregistered kind) with a label or an annotation on its own metadataBuilt: the items emitted, the list’s metadata dropped. Template delivery refused only helm.sh/hookthere, and still does, with its own textList has metadata of its own that its items cannot keep: annotations …; labels …Kindorapiversionbeside the exact key, on a document or on an item of a listBuilt, with two exceptions: emitted as written, the extra key kept. A workload or a claim was refused already, by Undeclared fields ( undeclared field Kind), and so was an item of a typed list whoseKindorapiversionstates another value than the list holds, by the parser (the item states …, the list holds …). Both are refused now with the other textthe key "Kind" equals "kind" only after case folding; write it "kind" or remove itItemson av1ListDecoded, to no object at all: a chart or a source with other objects built. A manifestsorcrdsource that held nothing else was refused already (source resolved to no manifests)The same text, for "Items"Itemson a list of an unregistered kindBuilt: the list emitted as one object The same text A nullitem in a list of an unregistered kindBuilt: an otherwise empty object emitted, with the list’s apiVersionand its kind withoutListitem 0 of WidgetList: the item is null, not an objectAn item of a registered kind, in a list of an unregistered kind, that does not decode as its type ( data: 1on a ConfigMap)Built: the item emitted as written. A workload, a claim or a PersistentVolume there did not build: the policy check refused it as unreadable, and it is refused now for the decode The item’s decode error, naming its position In a JSON document, an item of a registered kind, in a list of an unregistered kind, that states apiVersionorkindand thennullfor it, where the stated value is not the one its list gives itBuilt: the item emitted as written. A workload, a claim or a PersistentVolume there did not build: the policy check refused it as unreadable item 0 of FooList: Deployment "web": the object was read as apps/v1 Deployment, and the decode that checks its fields reads the document as example.com/v1 Deployment, so its fields cannot be checked; …(Undeclared fields, below)An object of an unregistered kind that does not end in List, with a top-levelitemsarrayBuilt: the entries emitted in its place, each unstructured … anitemsarray on an object of a kind that is no list (example.com/v1 Widget) is read as a list by what applies the output …Not refusals, and a change of output all the same: an item of a registered kind in a list of an unregistered kind is read as its Go type, as in a
v1List, where it was emitted as written. It is emitted from that type, Undeclared fields applies to it, and the policy check reads it: a workload, a claim or a PersistentVolume there was refused as unreadable and is now checked, and builds when the policy allows it. A list inside such a list was left whole, and refused by the policy check; it is opened in turn.Texts that changed: a
nullitem of av1Listwasnil runtime object providedand isthe item is null, not an object;… is nested more than 8 lists deepnow holds for a list of an unregistered kind too; the policy check’s refusal of an object with a top-levelitemsarray no longer says the objectsits inside a list of an unregistered kind, and no decode returns such an object any more (Limits, below).Output order. Every rendered manifest carrying a
helm.sh/hookannotation (or a standalonehelm.sh/hook-weight) is grouped by(phase, weight)via kure’shelm.SplitByHookWeight.Generatereturns every rendered object flat, in hook-execution order —pre-install, pre-upgrade, main, post-install, post-upgrade, <unknown, alphabetical>— with a multi-event annotation such aspre-install,pre-upgradeplaced by its earliest phase; a chart with no hook annotations at all yields one group, in chart-render order.pre-delete/post-delete/pre-rollback/post-rollback/testobjects are dropped outright (kure does not surface them as static manifests) — mostly a bug fix, since atestPod rendered as a static GitOps object would otherwise be reconciled on every apply, but it is silent data loss for a chart that relies on one of those hooks;pkg/oamhas no logging channel to flag it.Rendered objects. The render is decoded with kure’s manifest parser (
io.ParseYAMLWithOptions, unstructured allowed), the decode themanifestscomponent uses (go-kure/launcher#791). An object of a kind kure’s scheme registers is emitted as its Go type — a Deployment as*appsv1.Deployment, a hook Job as*batchv1.Job— and any other as unstructured. What follows from reading a document as the API server does:- a field the registered type does not declare is not dropped, as the parser alone would drop
it: the object is refused or emitted as rendered (Undeclared fields, below). A value of
the wrong type (an unquoted
trueas an annotation value) is a build error; - YAML is read as YAML 1.1, so an unquoted
yesoryis a boolean; a mapping key that is not a string (1:) becomes its string form, and an unquoted timestamp stays the string the chart wrote; in an unstructured object an integer an int64 cannot hold becomes a float; - a large integer survives the write where the object holds it as an integer. Since
go-kure/kure#1006 kure’s manifest writer writes an integer field of a typed object, and an
integer in an unstructured one, with its own digits:
9007199254740993and9223372036854775807are written as rendered (they were written9007199254740992and9223372036854776000). That holds for every object launcher writes, whichever component emitted it. A number the object holds as a float is written as that float, with the shortest digits that read back as it, and an integer an int64 cannot hold is such a float in an unstructured object (the item above):9223372036854775808is written9223372036854776000, and18446744073709551615as1.8446744073709552e+19. A value that large has to be a string in a field that takes one; - an empty, null or comment-only document is skipped, while a scalar, a sequence,
{}and a mapping withoutapiVersionandkindare build errors — in a document of a dropped hook as well, since the render is decoded before hooks are grouped; - a list document is replaced by its items, in the list’s order. What a list is, is kure’s
parser’s to say, in one place since go-kure/kure#1014: a list kind the scheme registers
(a
v1List, a typed list such asDeploymentList), and a kind the scheme does not register whose name ends inListand that statesitems. Each item of a typed list is decoded as the kind the list holds. Each item of av1Listand of a list of an unregistered kind is decoded as a document of its own: typed when its kind is registered, unstructured otherwise, and a list among them is replaced by its items in turn, to the eight levels the parser bounds. An item of a list of an unregistered kind that leavesapiVersionorkindout is read with the list’sapiVersionand the list’s kind withoutList. An item that does not decode, or isnull, is a build error naming its position (item 0 of WidgetList: the item is null, not an object). A list is never emitted, so what it states on its own metadata would be lost: a label or an annotation there is the parser’s error (List has metadata of its own that its items cannot keep: annotations helm.sh/resource-policy). A kind the scheme does not register that ends inListand states noitemsis one object; a registered list kind that states none is a list without items, and yields no object; apiVersion,kindand, on a list,itemsare read under those exact keys, and a key that equals one of them only after case folding (Kind,apiversion,Items) is the parser’s error, on a document and on an item of a list (the key "Kind" equals "kind" only after case folding; write it "kind" or remove it): the readers of a document downstream do not agree on which of two such keys stands;- an object of a kind the scheme does not register and that does not end in
List, with a top-levelitemsarray ({kind: Widget, items: [...]}), is a build error, with a policy or with none, at the top of the render and as an item of a list (Widget "w": anitemsarray on an object of a kind that is no list (example.com/v1 Widget) is read as a list by what applies the output, which would apply its entries in the object's place; write the entries as documents of their own, or give the object a kind ending in List). The parser reads such a document as one object. What applies the output tells a list by that array and not by the kind, so the entries would reach the cluster without any check here having read them. It is the envelopepassthroughrefuses (Lists, underpassthrough), and the one refused on a registered kind that declares noitems(Undeclared fields, below). Anitemsthat is no array, or one below the top level, is the object’s own content. The refusal is no refusal by the policy and carries no class. Under template delivery it is still the component’soam.ViolationError, as every render that does not decode is, since the chart is rendered at the transform’s policy step; amanifestssource reports it as its parse error, without one; - a document the decoder of its kind panics on is a build error, not a crash. The API type
of a registered kind may decode itself and not handle what was written: Cilium’s ICMP
field dereferences the
typeanicmpsfield of aCiliumNetworkPolicyor aCiliumClusterwideNetworkPolicyleft out. Since go-kure/kure#1009 kure’s parser reports the panic itself, as that document’s parse error. The error names the object by its kind and its name and holds the panic (decoding rendered manifests: parse error in Kubernetes object: failed to decode object: the decoder panicked on CiliumNetworkPolicy "demo/p": runtime error: invalid memory address or nil pointer dereference); it names neither the field nor the document’s position, and an item of a list by its position in the list (item 0 of List). Such a document is one bad document among the others: the error holds every document’s own, and the YAML error of input that stops being YAML further down. Before that change launcher caught the panic itself, named the document by its position (document 2 (CiliumNetworkPolicy "demo/p")) and reported that document alone; - malformed JSON the decoder cannot read past is a build error:
{], alone or after JSON documents. What follows it in the input is not read. Before go-kure/kure#1012 kure’s parser did not return on such input, and neither did the build; - which kinds are registered is kure’s to say. Since go-kure/kure#1007 a MetalLB
BGPPeeratmetallb.io/v1beta2, the version MetalLB stores, is one, besidev1beta1. Breaking for a chart or a manifest source that holds such a document, which was emitted as rendered: it is now emitted as its Go type, sospec.passwordSecret: {}is written where the document left the field out, and a field of the wrong type (holdTime: [1]) is a build error. One that sets an undeclared field is still emitted as rendered (Undeclared fields, below); - a list document where a
helm.sh/hookannotation is involved is a build error naming the list: the annotation on the list’s own metadata, or on one of its items (for av1Listand a list of an unregistered kind, whose items are documents of their own, at every depth the parser opens). Helm reads a hook on the rendered document’s own metadata and nowhere else, and the parser reads only a list’s items. So the items of a hook list would be emitted as ordinary resources, and an item’s own annotation, which Helm does not read, would group it as a hook or drop it. The check reads a list as the parser does, with one addition: Helm readsmetadataunder any case of the key and the parser reads the exact key, so a hook on a list underMetadatais refused here. A list without the annotation, and without any other label or annotation of its own, builds; an object that is not a list keeps its hook.
A decode failure is reported as
decoding rendered manifests: ….Generatereturns a fresh copy of the decoded objects on every call, asmanifestsdoes, so a trait that decorates a typed workload (topology-spread,security-context, a mountedconfigmap,external-secret) acts on a chart’s Deployment as on an authored one, and generating the same result twice gives the same output.Policy.
ApplyPolicyholds the chart to the environment policy in two steps (go-kure/launcher#791); the transform calls it withNoopPolicywhen no policy is passed.- The source, before any fetch. The host of
source.urlmust match an entry of the policy’s allowed registries (AllowedRegistries) exactly, as foroci,crdandmanifests; for anoci://URL that is its first segment, which is what the Helm registry client pulls from. A refused source fails before any request is made: only an allowed source is rendered. No allowlist, or an empty one, permits every host. - Every emitted workload. The chart is then rendered — in the transform, so a fetch or render
failure is reported there, as that component’s policy error — and each Pod, PodTemplate,
ReplicationController, Deployment, StatefulSet, DaemonSet, ReplicaSet, Job and CronJob in it,
a kept hook’s included, is checked as an authored workload is: host namespaces, hostPath
volumes, the cpu/memory maxima, the storage maximum on a generic ephemeral volume’s claim,
the registry allowlist on an image volume’s reference (breaking, go-kure/launcher#790:
a chart that renders one from a registry outside the list built before; see Image volumes
under pod),
and for every init and regular container the registry allowlist, the privileged, HostProcess
and capability gates, and
ValidateImageRef(no untagged image, no:latest), which holds the reference of an image volume too (breaking, go-kure/launcher#790: a chart that renders an untagged or:latestone built before). Ephemeral containers are refused. The storage a PersistentVolumeClaim, or a StatefulSet’s claim template, requests is held to the storage maximum (MaxStorageSize), as thepersistentvolumeclaimandstatefulsetkinds hold theirs. The replica count of a Deployment, StatefulSet, ReplicaSet or ReplicationController (one when the chart sets none) and themaxReplicasof a HorizontalPodAutoscaler, in any API version, are held to the replica maximum (MaxReplicas), as thedeploymentandstatefulsetkinds and thescalertrait hold theirs. A PersistentVolume is held to what thepersistentvolumekind holds its own to: ahostPathorlocalsource needsAllowHostPathVolumes(), andspec.capacity.storageis held to the storage maximum (breaking for a chart that renders one; see persistentvolume). The error names the rendered object and the field (helmtemplate: rendered Deployment "demo/web": spec.template.spec.containers[0] "app": …).
Behaviour change: a chart that renders a privileged container, a host namespace or a hostPath volume no longer builds unless the policy allows it — and with no policy passed nothing allows it, since
NoopPolicydenies all five. The policy accessors that allow such a chart areAllowPrivileged(),AllowHostNetwork(),AllowHostPID(),AllowHostIPC()andAllowHostPathVolumes(); there is no per-chart exemption. A chart image without a tag, or tagged:latest, has to be pinned through the chart’s values.Undeclared fields (go-kure/launcher#794, item 7). Kure’s parser decodes a registered kind leniently: a field the vendored API type does not declare — one a newer Kubernetes version added, say — is dropped, with no error. The policy check reads that type, so it would pass a workload whose undeclared pod spec field it never saw, and the object would be written without the field. Template delivery,
manifests,crdandpassthroughtherefore decode each document of a registered kind once more, strictly, over the same scheme, and act on what that reports:- A workload or a claim — the kinds the policy check reads as Go types: Pod, PodTemplate,
ReplicationController, Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob,
PersistentVolumeClaim and PersistentVolume, which “a workload or a claim” stands for
wherever this rule is cited — that sets an undeclared field is refused, under any policy
and with none. A kind the check comes to read as its Go type joins them. The error names the object and the path of every such field (
decoding rendered manifests: Deployment "demo/web": undeclared field spec.template.spec.fieldOfALaterVersion: the apps/v1 Deployment type this build reads the object with does not declare it, so the object cannot be checked against environment policy). - Any other registered kind is emitted as the document was rendered, unstructured, the
field kept. A HorizontalPodAutoscaler is one of these: the check reads its
maxReplicasfrom the unstructured object as it does from the Go type. One shape is refused: a top-levelitemsarray on a kind that declares none, since written out the object is a list to whatever applies it, and its items would be applied in its place.
A list (a
v1List, a typed list, a list of an unregistered kind) is replaced by its items, so the rule is each item’s: an item is refused or kept as a document of its own is, the error naming its position (decoding rendered manifests: item 1 of List: Deployment "demo/web": undeclared field …;item 0 of WidgetList: item 0 of List: …for a list in a list). A kept item that leftapiVersionandkindout is written with the ones its list gives it: the kind a typed list holds, and for a list of an unregistered kind the list’sapiVersionand its kind withoutList. The list’s other fields are not read: the list is never emitted. A document whose items cannot be matched to the objects the parser made of them is refused, not passed with an item unread.A key written twice is not an undeclared field and is read as before (the last value stands). One case of it is refused, whatever the kind: a JSON document that writes so many keys twice that the strict decode’s record of errors, which holds a hundred, is full without naming an undeclared field. Whether such a document also sets one cannot be told. (A document whose undeclared field is recorded before the record fills is treated as any other: refused as a workload or a claim, kept as another kind.)
Another is an item of a list of an unregistered kind, in a JSON document, that states
apiVersionorkindand thennullfor it. The parser reads the last statement, takes thenullfor the key left out and gives the item its list’s; the strict decode keeps the string thenullfollows. Where that string is what the list gives too, the two agree and nothing changes. Where it is not, the strict decode checks the fields of another kind than the one emitted, and an item the parser made a Go type of is refused, with or without an undeclared field (item 0 of FooList: Deployment "web": the object was read as apps/v1 Deployment, and the decode that checks its fields reads the document as example.com/v1 Deployment, so its fields cannot be checked; a second apiVersion or kind that is null is the known cause, state each once). The parser refuses the same in an item of a typed list itself. An item the parser leaves untyped, its kind not registered at the list’sapiVersion, is emitted as the parser read it and is not checked as the kind the strict decode would read: nothing of an unstructured object is dropped.Whether a cluster accepts a kept field its own version does not know is not verified here; the object reaches it as the chart wrote it.
Breaking, in two ways. A chart that renders a workload or a claim with an undeclared field built before, the field dropped from the output, and no longer builds: the field has to go, or wait for a launcher whose pinned API types declare it. And the output of every other registered kind gains the fields the typed decode had dropped since go-kure/launcher#791; such an object is written from the document and not from its Go type, so the rest of it is as the chart wrote it too.
Known limit: a type that unmarshals itself decodes its own keys, so an undeclared key inside one is not reported and is still dropped. A CustomResourceDefinition’s
itemsschema is such a type: the same kind of type as aCiliumNetworkPolicy’s endpoint selector, which the kind inventory records and an authored Cilium policy checks on its own. A key next topropertiesin the same schema is reported and kept. No type under a workload or a claim does this beyond scalar leaves (Quantity,IntOrString,Time) and the raw field set ofmanagedFields; a test walks those types and fails when one starts to.Limits. The items of a list, whatever its kind, are checked as documents of their own are, a list among them opened in turn. A workload, claim or PersistentVolume in an API version kure’s scheme does not register (
batch/v1beta1,apps/v1beta2) cannot be read and is refused rather than passed unchecked, inside a list as outside one. The decode returns no object with a top-levelitemsarray (Rendered objects, above), so the check meets none; one that reaches it another way is refused as unreadable. Not checked: an object of a dropped hook (never emitted); the pods a custom resource’s controller creates, and the replica count a custom resource sets; the host of the chart archive a Helm repository’s index points at, and any redirect, which kure’s renderer follows. That one is a decided limit (go-kure/launcher#794, item 6): the allowlist is checked on the chart’s source URL before anything is fetched, and holding every later request to it needs a hook in kure’s chart renderer, which takes a release name and a namespace and nothing about its requests (helm.RenderChart). A nil policy (a directApplyPolicy(nil), orGenerateon a config no policy was applied to) checks nothing, andApplyPolicy(nil)withdraws no policy applied before. A chart delivered as a FluxHelmRelease(helmrelease,helmunderdelivery: flux) is rendered on the cluster, so nothing it renders can be checked at build time; only the host of a source component in the document is.Hook-group layout. For a layout-walking consumer (the
layout.LayoutAugmenterpath), more than one hook group makesAugmentLayoutclear the component’s flatResourcesand replace them with one childManifestLayoutper group, named<application>-<component>-NN-<phase-slug>, written to its own directory<component dir>/<child>(itsNamespaceis the component layout’s own path, which kure joins with the child’s name, so a hook-group directory is never nested twice) and chained viaDependsOn, listing each child’s preceding sibling in the order kure’shelm.SplitByHookWeightsynthesizes from Helm’s hook phases — a combined install/upgrade ordering for GitOps reconciliation, not Helm’s own per-operation execution order (kurepkg/stack/helm/hooks.go:28-36).DependsOnis set on every child, and only kure’sFluxIntegratedPerLayoutplacement carries it: there kure’s layout integrator gives each child a Flux Kustomization CR of its own and writes each entry into itsspec.dependsOnas the name of the Kustomization of the sibling the entry names (kurepkg/stack/layout/manifest.go’sDependsOnfield doc). UnderFluxSeparateandFluxIntegratedPerBundlea child gets no Kustomization of its own, and kure’s Flux integration refuses the child’sDependsOnandKustomizationName, naming the child (go-kure/kure#1032); before, it dropped them without an error, and the order between hook groups was lost. A chart with more than one hook group therefore needsFluxIntegratedPerLayoutunder kure’s Flux integration. When it partitions,AugmentLayoutalso sets the component layout’sApplicationFileModetoAppFilePerResourceunless the caller already set one, so the component stays a directory whosekustomization.yamllists the children. Without that, a writer-wideAppFileSingledefault (kurelayout.Config.ApplicationFileMode) would make kure’sWriteManifestrefuse the tree under any placement butFluxIntegratedPerLayout: anAppFileSinglelayout writes nokustomization.yaml, so nothing would list its child layouts (go-kure/launcher#563). The children carry no mode of their own, so under such a default each hook group is written as one file,<component dir>/<child>.yaml, instead of a sub-directory, and the component’skustomization.yamllists it; underFluxIntegratedPerLayoutkure’s layout integrator keeps every child a directory with its own Flux Kustomization, whatever the default. kure names that Kustomization<bundle's Kustomization>-<child>and shortens it past 63 characters by its own rule (go-kure/kure#1030, go-kure/kure#1036), while the child’s name, its directory, is held to 253 (pkg/oam/README.md, “Pipeline”). A single-group chart’sAugmentLayoutis a no-op. Everyhelmtemplatecomponent is aLayoutAugmenter, a hook-free chart included, since the group count is known only after the render — so even a hook-free chart gets its own sub-layout directory under a layout-walking consumer, for no behavioural benefit. The child name begins with the application name (go-kure/launcher#792): component names are unique only within one Application, while the Kustomization CRs a consumer generates for the hook-group children of every application can share one namespace, so two differently named Applications that each have a componentdbget<application>-db-NN-<phase-slug>children that differ. The transform hands the config its application (oam.ApplicationNameSetter, theApplicationfield); aHelmTemplateConfigbuilt directly, outside a transform, has none unless the caller sets the field, and its children are then named<layout name>-NN-<phase-slug>. Breaking output change: every hook-group child name, and so its directory and the Flux Kustomization a consumer derives from it, gains the leading<application>-. The Kustomization kure generates for the child underFluxIntegratedPerLayoutis<bundle's Kustomization>-<child>(shop-shop-db-01-main), and the next group’sspec.dependsOnfollows it. Breaking output change (go-kure/launcher#941): from go-kure/launcher#787 launcher named it by the child alone (shop-db-01-main) and shortened it to 63 characters itself; it now leaves the name to kure, whose<unit>-keeps two applications of one name in different bundles apart, and which refuses the name under the coarser placements (go-kure/kure#1032). A layout name over 253 characters is shortened by the one shortening rule (oam.ShortenNameWithSuffix, whichhelmuses for its values ConfigMap name): the-NN-<phase-slug>suffix is kept whole and<application>-<component>becomes its own beginning plus 10 hex digits of its sha256 (8 before go-kure/launcher#793), so two long names that differ anywhere almost never shorten to the same one. Known limitation: the two names are joined by a plain-, which either may contain, so applicationa-bwith componentcand applicationawith componentb-cstill get the same child names. The default carries no namespace either: like the bundle name, a child name is built from the application’s name alone, so two Applications with one name in different namespaces get the same child names. Keeping those apart is the author’s or the consumer’s: thehookGroupNamePrefixproperty, or theNaminghook’s answer for thehook-grouprole (go-kure/launcher#787), replaces<application>-<component>in the name of every child, directory and Kustomization alike. Such a prefix is a DNS-1123 subdomain and is never shortened: a child name over 63 characters built from it fails the transform, in an error with the component, the role and the full name (*oam.HookGroupNameError, which answers tooam.ErrHookGroupNameTooLong), and two components of one document that resolve to one prefix fail the transform. The transform has the chart rendered by its policy step, with or without a policy of the caller’s, and asks the config then (CheckHookGroupNames);AugmentLayoutreturns the same error for a config built directly or a prefix set after the transform. Known limit: the transform holds the prefixes apart, not the names built from them after the render, so two different prefixes can still give one Kustomization name (a written prefix that equals kure’s name for another component’s default child, less its suffix; a prefix that ends as another chart’s phase begins). kure refuses that name, used twice, when the walked tree is integrated; anotherhookGroupNamePrefixon one of the components is the way out (pkg/oam/README.md, “Name roles and theNaminghook”). AHelmTemplateConfigbuilt directly setsHookGroupNamePrefix. The component’s own layout, the parent of the hook-group children, also gets a Flux Kustomization underFluxIntegratedPerLayout, which kure names<bundle>-<component>and shortens past 63 characters by its own rule; launcher leaves that default to kure. Breaking output change (go-kure/launcher#941): before, the transform shortened a default over 63 characters by launcher’s rule andAugmentLayoutset it on the layout, so such a layout’s Kustomization name changes. ThelayoutKustomizationNameproperty, or theNaminghook’s answer for thelayoutrole, sets another name, a DNS-1123 subdomain of at most 63 characters used as written and never shortened; any other fails the transform, naming the component and the role. A name set on the layout needs per-layout placement: under any other the layout gets no Kustomization of its own, and kure’s Flux integration refuses the name, naming the layout (go-kure/kure#1032). AHelmTemplateConfigbuilt directly setsLayoutKustomizationName;AugmentLayoutnever overwrites aKustomizationNamethe layout already carries.GenerateCoversAugmentLayoutis always true —Generate’s output is already the flat unionAugmentLayoutrepartitions — sokurel build, which never walks a layout, accepts the component and emitsGenerate’s flat output.- a path set in both trees is refused, naming the path
(
oci —
source.url(oci://…),source.name(a name for the generated OCIRepository, see below),source.objectName(a name for the OCIRepository the component keeps to itself, see below),kustomizationName(a name for the Kustomization, see below),version(tag orsha256:…),path,prune,interval,targetNamespace,wait,healthChecks.Lowering (go-kure/launcher#784).
ociis a role-named component:OCIRulelowers it to the two kind-named terminals it stands for, anocirepository(url,ref.tagfromversion, orref.digestfor asha256:value, and the authoredinterval) and afluxcd-kustomization(path,prune,sourceRef, andinterval,targetNamespace,waitandhealthCheckswhen set). No handler is registered foroci. The rule runs the checks below first, with theiroci:messages; everything past them is the terminals’ own.A component alone on its artifact lowers to a same-name group: both objects are named after the component (unless
kustomizationNameorsource.objectNamenames them, below) and deploy as one unit, the OCIRepository first. This is the pair of objectsocihas always emitted, byte for byte. Annotations go to both members, so a tier annotation, aplacementpolicy or adependencyrule naming the component acts on the pair.prune-protectionandforce-replacecover both objects (the pair’s application takes the delivery intent); every other trait goes to the Kustomization.Two or more
ocicomponents of one document on the same source share it. The source then belongs to the document, not to the component that comes first: it is emitted once, named<document>-source-<digest>(the 10-hex digest of the source identity, throughLoweringContext.ResolveSharedName, which shortens a default over 253 characters by the one rule), and each component lowers to its Kustomization alone, referencing it and ordered after it (Component.OrderAfter). Like a sourcehelmgenerates, it is then a generated source:pkg/oamapplies it with the application bundle, ahead of every group, wherever its consumers are placed. It carries no trait and no annotation of any consumer, and may not be placed in a tier or made to wait on a component. The consumer’sNaminghook may rename it: it is asked under rolehelm-source, once for each source identity of a document, with that default and no component (go-kure/launcher#787).source.namenames the source (go-kure/launcher#787), and makes it the shared form whatever the number of consumers: the OCIRepository is emitted once under that name, applied with the application bundle ahead of every group, and the component lowers to its Kustomization alone. A source named this way carries no annotations and no traits of the component, not even for a component alone on its artifact:prune-protectionandforce-replacethen cover the Kustomization only, and a tier annotation places only the Kustomization. The name is the component’s own choice. Components that write the same name for the same identity share the source; a component that writes none does not share it and is not counted among its consumers, so it keeps its own source, or shares the document’s with the other components that name none. A name equal to that shared source’s own names that source: one name for one identity is one source. The name is used as written: it must be a DNS-1123 subdomain of at most 253 characters, and cannot be the component’s own name or that of another component of the document (oci: source.name "base" is the component's own name; the generated source is a component of the document too, so give it another name). One name for two identities is refused as a collision.source.name: ""reads as absent.kustomizationNameandsource.objectNamename the two objects that carry the component’s name (go-kure/launcher#787): the Kustomization, in both forms, and the source a component keeps to itself. Each is the author’s property, else the consumer’sNaminghook’s answer (rolesoci-kustomizationandoci-source, asked with the component), else the component name, so a document that sets neither and a hook that declines give the same output as before. A name that is not the default is used as written: a DNS-1123 subdomain of at most 253 characters, refused otherwise and never shortened. It names the object alone. Both members keep the component’s name, so the pair stays one unit: a tier annotation, aplacementpolicy or adependencyrule naming the component still acts on both, and the traits go where they went. The Kustomization’ssourceRefnames the kept source by the name that object takes. Every one of these names is claimed, the default included, so an authoredocirepositoryorfluxcd-kustomizationcomponent whoseobjectNameis the same name is refused in the transform with both named:source.objectNameandsource.nameboth name the OCIRepository, in two ways, and are refused together. Writesource.objectNameto rename the source the component keeps to itself: it stays with the Kustomization and carries the component’s annotations and itsprune-protectionandforce-replacetraits. Writesource.namefor a source generated on its own, which components that write the same name share. A component that writessource.objectNamekeeps its own source whatever other component has the same identity, and is not counted among the consumers of a shared one.The source identity is the
url, theversionand the effectiveinterval. Unset,0s,60mand1hare one interval. Components whose intervals differ do not share: each keeps a source of its own, polling at its own interval, as a component alone on its artifact does. Anociand ahelmcomponent on one artifact never share either: the identities differ, and a Helm source copies the chart layer instead of extracting it.
Output changes. For a document in which several
ocicomponents reconcile one artifact, the OCIRepository is renamed from the name of the component deployed first to<document>-source-<digest>. It is applied with the application bundle, ahead of every group, instead of with that component, so the application has an ordered group even when the document declares no order. That component’sprune-protectionorforce-replaceno longer covers it. The rename also happens when a second consumer is added to a document that had one, and is undone when it is removed. Components that shared one source while their intervals differed now emit one source each.interval: 0snow emits the 60m default on both objects, since both terminals read a zero interval as unset; it used to be emitted as0s. Nothing else changes.Sharing is decided among the
ocicomponents the document holds when the rule runs: anocicomponent a third-party rule emits in a later lowering round is not counted as a consumer of a source lowered before it.Messages. The checks the rule runs keep their
oci:text. A lowering refusal is reported with the component and its document (component "x" (type "oci") in document …: oci: version is required …). The registry allowlist is theocirepositoryterminal’s check, so its refusals name that kind and its field (ocirepository: url: …) rather thanoci: source.url.targetNamespacehas no default (go-kure/launcher#622). Unset, the Kustomization emits nospec.targetNamespaceand each object keeps the namespace the artifact’s own kustomize build gives it: the one it carries, or anamespaceset in the artifact’skustomization.yaml. kustomize-controller does not fill in its own namespace for an object that still has none. AuthortargetNamespacewhen namespaced objects are left without a namespace after that build: they otherwise fail at apply withnamespace not specified, with or without a Flux namespace. This deliberately differs fromhelmrelease, which under a Flux namespace defaultstargetNamespaceto the application namespace. A Kustomization’stargetNamespacesets or overrides the namespace of every namespaced object it applies, Flux custom resources included, so a default would move the objects of a deliberately multi-namespace artifact.waitandhealthChecksare opt-in readiness settings for the delivery Kustomization (go-kure/launcher#432). A document in which neither requests anything —waitabsent orfalse, andhealthChecksabsent, null or empty — builds the same Kustomization byte for byte, and the default stays unset.wait: truesetsspec.wait, so Flux reports the Kustomization ready only once everything it applied is ready;false, like an omitted key, emits nothing.healthChecksis a list of{apiVersion, kind, name, namespace}entries copied, in authored order, intospec.healthChecks. The component delivers an opaque artifact, so the list is authored, never derived.apiVersion(apps/v1, orv1for a core kind),kindandnameare required non-empty strings, andnamespaceis optional, left out for a cluster-scoped kind. Any other key in an entry is refused by name, a wrongly typed field is a type error (healthChecks[0].name: must be a string, got int), and an empty or null list emits nothing.wait: truetogether with a non-emptyhealthChecksis refused: kustomize-controller ignoreshealthCheckswhenwaitis true, so the list would never be checked. There is notimeoutproperty; Flux’s own default applies. Under a policy with a non-empty registry allowlist (AllowedRegistries),source.urlmust name its registry explicitly —oci://<registry>/<repository>with a non-empty repository and a registry that islocalhostor contains.or:— and that registry must match an allowlist entry exactly (go-kure/launcher#580). Flux’s source-controller parses the url with go-containerregistry’sname.NewRepository, which reads any other first segment as part of a Docker Hub repository:oci://ghcr.ioandoci://registry/my-artifactare pulled from Docker Hub, so they are refused even whenghcr.ioorregistryis listed. No policy, or an empty allowlist, accepts everyoci://url.interval(default60m) must be a duration Flux accepts, checked exactly as forhelmrelease(go-kure/launcher#590): see its Defaults paragraph.fluxcd-kustomization — the kind-named terminal for Flux’s
Kustomization(go-kure/launcher#784). Its properties are exactly the top-level JSON keys ofKustomizationSpecin the kustomize-controller API versiongo.modlinks; a test ties the published schema to that struct, so a bump that adds or drops a spec field fails the suite until the schema follows. It is an authored Flux object, likehelmrelease: it does not deliver the application it is part of, and it creates no source —sourceRefnames one that exists, authored beside it (ocirepository,gitrepository,bucket) or already on the cluster. The type isfluxcd-kustomizationrather thankustomization, which would read as thekustomization.yamlfile (go-kure/launcher#352).Decoding. The whole property map is decoded with
builtin.DecodeStrictJSONintoKustomizationSpec, sopatches,postBuild,dependsOn,force,timeout,serviceAccountName,decryption,kubeConfigand every other field are emitted as authored. A key the struct does not declare, at any depth, is refused with its path from the property root (unknown field "patches[0].target.kinds",builtin.UnknownJSONFieldPath): the spec declareskind,nameandnamespacein several places, so the bare key would not say where. A wrongly typed value is refused too. Keys match case-insensitively, as inencoding/json; schema validation, which akurel buildruns first, is exact.Defaults and checks.
intervaldefaults to60mwhen unset (a zero duration counts as unset); Flux requires the field.interval,retryIntervalandtimeout, when set, must be durations Flux accepts, checked as onhelmrelease.sourceRef.kindis required and one ofOCIRepository,GitRepository,Bucket,ExternalArtifact, the CRD’s enum;sourceRef.nameis required.pruneis a required field of the spec with no default here: unset,falseis emitted (ocipassestrueunless told otherwise).targetNamespaceis never defaulted, with or without a Flux namespace, for the reason given under oci. Nothing else is checked: the enums and the cross-field rules are left to the Kustomization CRD’s own admission, andwait: truebesidehealthChecks, whichocirefuses, is emitted as authored (kustomize-controller then ignores the list). Whether the source exists is known only on the cluster. The checks run again inGenerate, for a config built directly.Identity and namespaces. The Kustomization is named after the component and lands in the Flux namespace when one is configured, else in the application namespace (
SetFluxNamespace). The ConfigMaps and Secrets it reads from its own namespace —decryption.secretRef,kubeConfig.secretRef/configMapRefand eachpostBuild.substituteFromentry — must live there too: aconfigmap,external-secretorcertificatetrait on the component whose object one of them names moves with the Kustomization (FluxNamespaceReads, go-kure/launcher#740).ApplyPolicychecks nothing: a Kustomization fetches nothing itself, and the registry allowlist applies where its source is authored.helmrepository / ocirepository / gitrepository / bucket / helmchart — the kind-named terminals for Flux’s four fetching source kinds (go-kure/launcher#347, part of the Helm-family redesign go-kure/launcher#336) and for its
HelmChart(go-kure/launcher#351), which builds a chart artifact from one of them. Each component’s properties are exactly the top-level JSON keys of its source-controller spec type —HelmRepositorySpec,OCIRepositorySpec,GitRepositorySpec,BucketSpec,HelmChartSpec, in the versiongo.modlinks — and a test ties each published schema to its struct, so a bump that adds or drops a spec field fails the suite until the schema follows. Each emits exactly one CR of its kind, named after the component, and nothing else: no Kustomization (compareoci), no HelmRelease (comparehelmrelease), no source for ahelmchart(itssourceRefnames one that exists, as ahelmrelease’schart.spec.sourceRefdoes), and no source dedup — two components naming the same URL emit two CRs. Ahelmreleaseconsumes ahelmchartthroughchartRefwithkind: HelmChart.Decoding. The whole property map is decoded with
builtin.DecodeStrictJSONinto the spec type, with no launcher-owned keys. A key the struct does not declare, at any depth (secretRef.namespace, a stray key in a GitRepositoryincludeentry), is refused by name, and so is a wrongly typed value (suspend: "yes", an unparsable duration). The schema keeps the nested Flux blocks as open objects; the strict decode is what checks them. A reflection test asserts, per kind, that no spec field is unreachable through that decode, against an explicit exclusion list that is empty. Keys match case-insensitively, as inencoding/json; schema validation, which akurel buildruns first, is exact.Checks and defaults. Required:
urlonhelmrepository,ocirepositoryandgitrepository;bucketNameandendpointonbucket;chart,sourceRef.kindandsourceRef.nameonhelmchart. Aurlmust start with a scheme the CRD’s own pattern admits:http://,https://oroci://forhelmrepository, and onlyoci://undertype: oci;oci://forocirepository;http://,https://orssh://forgitrepository, so an scp-stylegit@host:org/repois refused — writessh://git@host/org/repo. The CRD has no pattern forendpoint, so it is only required.intervaldefaults to60mwhen unset (a zero duration counts as unset), matching thehelmreleasedefault; ahelmrepositorywithtype: ocigets no default, since Flux does not poll it, and its emittedintervalreads0s, the value the Go type always writes. A setintervalmust be a duration Flux accepts, checked exactly as onhelmrelease(go-kure/launcher#601); undertype: ocitoo, where the CRD pattern still applies. A settimeout(ahelmcharthas none) is checked the same way, against the source CRDs' narrower pattern, which has nohunit:^([0-9]+(\.[0-9]+)?(ms|s|m))+$(go-kure/launcher#606), so an authored1his refused. Atimeoutof an hour or more authored in minutes or seconds (90m,3600s) is accepted and emitted with its hours folded into minutes:90m0s,60m0s(go-kure/launcher#619). Ametav1.Durationwould writeDuration.String(),1h30m0s, which the CRD rejects, so such a source is emitted as an unstructured object carrying that text; atimeoutbelow an hour, or none, leaves the source emitted exactly as before. A config built directly gets the same minutes form. An authoredverifythat names noprovider, on anocirepositoryor ahelmchart, is written withprovider: cosign, the API’s default, and an authoredprovider: ""is refused (go-kure/launcher#790), for the reason given onhelmreleaseabove: the Go type writes the field whether or not it was authored, so the API’s default never applies. A config built directly with an empty provider gets the default too.TestKindComponents_OmittedRequiredAndWrittenDefaultsholds the five source kinds, withhelmreleaseandfluxcd-kustomization, to the required fields and the defaults the markers of the linked Flux modules state, in the test’s two sets only: a required field the type leaves out when it is empty, and a default the type writes over unauthored. The seven have four such fields:chart.spec.sourceRef.kindonhelmrelease, refused, andverify.provideron that kind’s chart, onocirepositoryand onhelmchart, filled. Nothing else is checked or defaulted: enums (type,provider,layerSelector.operation,verify.mode, a HelmChart’ssourceRef.kindandreconcileStrategy) and cross-field rules (a Bucket’sstsagainst itsprovider,serviceAccountNameagainstsecretRef, a HelmChart’sverifyonly with a HelmRepository source) are left to the CRD’s own admission. Nor is the source ahelmchartnames: whether it exists, or is of the kindsourceRef.kindsays, is known only on the cluster.Policy.
ApplyPolicychecks the host the source is fetched from against the policy’s allowed registries (AllowedRegistries), with the same exact matchoci,crdandmanifestsapply: theurlhost for the three repositories, theendpointhost forbucket. A port is part of the host, soregistry.local:5000matches only an entry with that port. The user of anssh://URL is dropped (ssh://git@github.com/org/repochecksgithub.com); userinfo on anhttp://,https://oroci://URL is not, so such a URL matches no entry and is refused. An empty allowlist permits every host, and none of the rules below applies. Ahelmchartchecks nothing: it fetches from the source itssourceRefnames, whose host is checked where that source is authored, as for ahelmrelease.No refusal prints the value it refused, since a URL or
endpointcan carry a credential in its userinfo (the user included), path, query or IPv6 zone (go-kure/launcher#699). An allowlist refusal names the host asdisplayHostreduces the url’s raw authority (anssh://user included, not the host the match used): without userinfo, query, fragment or zone, port kept, saying when something was dropped; the matching itself is unchanged. It names no host at all when the reduced text is not a plain host (a DNS name, IPv4 address or bracketed IPv6 address, with an optional numeric port), or when a[,],?or#, none of which a userinfo may hold, precedes the last@, which may then sit inside IPv6 brackets or a query instead. A scheme refusal (the four sources’,oci’s, andcrd’s andmanifests’http/httpscheck) and the explicit-registry refusal below name only the component, the field and the form expected, and the Amazon S3 refusal names the reduced host.Some sources are not fetched from the host their field names, so the check follows Flux instead:
- An
oci://URL must name its registry explicitly. Flux parses an OCIRepositoryurl, and a Helm OCI chart reference when it verifies the chart’s signature, with go-containerregistry, which takes the first path segment as the registry only when a/follows it and it islocalhostor contains.or:; anything else is a Docker Hub repository. Sooci://ghcr.ioandoci://registry/appare pulled from Docker Hub, whichever entry their first segment matched. Under a non-empty allowlist such a URL is refused, with an error asking for a qualified registry (oci://docker.io/library/app,oci://registry.example:5000/org/app). Anocirepositoryurlalso needs a repository path after the registry; ahelmrepositoryoci://URL, of eithertype, may stop at the registry (oci://ghcr.io), since Flux appends the chart name to it. - A
bucketwithprovider: gcpis checked againststorage.googleapis.com. Flux’s GCP client never readsendpoint; it uses Google Cloud Storage’s own host. Thegeneric,awsandazureproviders, and no provider, fetch fromendpoint, which stays the host checked, except as the next rule says. - A
bucketwith an Amazon S3endpointis refused, unless its provider isazureorgcp. The S3 client behindgeneric,awsand no provider does not fetch from an Amazon S3 endpoint: it contacts the S3 host ofregion, which may name any AWS region or partition, or of the bucket’s own location whenregionis unset, chosen at runtime. That host cannot be checked, so under a non-empty allowlist the build fails, even when the allowlist lists the endpoint. The test errs broad, since it fails closed: anyendpointcontainingamazonaws, in any case, counts, because the client’s own host patterns also match look-alikes such ass3.x-amazonaws.com.
Hosts a source reaches indirectly are not checked: the chart URLs a Helm repository index advertises on other hosts (what
passCredentialsexists for), a Bucket’ssts.endpoint, the token endpoints aproviderauthenticates against, and a proxy. Apart from the twobucketcases above, aprovider,regionorinsecuresetting changes how the source authenticates or which scheme it uses, not which host it fetches from.Namespace and references. The CR lands in the Flux namespace when one is configured, else in the application namespace (
SetFluxNamespace). Every local reference it carries —secretRef,certSecretRef,proxySecretRef, the Secret references underverifyandsts,serviceAccountName, a GitRepositoryinclude, a HelmChart’ssourceRef— resolves in the namespace the CR lands in, so under a Flux namespace the objects it names must live there. AhelmchartwhosesourceRefnames a source component in the same document finds it there: both move to the Flux namespace. So does the Secret of anexternal-secretorcertificatetrait (or the ConfigMap of aconfigmaptrait) on the component when one of the Secret references names it — acertSecretRefnaming acertificatetrait’ssecretNamemoves that Certificate (FluxNamespaceReads, go-kure/launcher#740); a trait object no reference names stays in the application namespace.Generated by the
helmrule. An inline source on ahelmcomponent generates one of these components: ahelmrepositoryfor anhttp(s)://URL; anocirepositoryfor anoci://URL, with aref.tagfromversionand alayerSelectorcopying the Helm chart content layer; agitrepositoryforkind: GitRepositorywith its URL and onesource.reffield; or abucketforkind: BucketwithendpointandbucketNameand no URL. It is named<app>-source-<digest>and shared by everyhelmcomponent with the same source identity (see helm). An authored source component exposes the whole spec (credentials,type: oci,provider, verification, …) and is never shared. The rule never generates ahelmchart.- An
postgresql —
provider: cnpg,version(default16),storageSize(precedence: authored > policy defaultstorageSize>1Gi),replicas,backup.*,monitoring.enabled,pooler.enabled,poolerName,managedRoles,databases(each with an optionalobjectName),clusterObjectName,objectStoreObjectName.clusterObjectNamenames the Cluster in place of the component name. CloudNativePG derives the Cluster’s Services and Secrets from the Cluster’s name, the default backup path moves with it, and renaming an existing Cluster creates a new one: the old one is pruned with its data unless it is protected (see “The Cluster’s and the ObjectStore’s names” below). A component lowering rule (PostgresqlRule,postgresql_lowering.go, go-kure/launcher#281), not a dispatchable handler: it reads the properties as before (Parse; the published schema is pinned byte for byte,testdata/postgresql-property-schema.json: the former handler’s, withpoolerName,clusterObjectNameandobjectStoreObjectNameadded) and emits the CNPG kind components that build the same objects — acnpg-clusterunder the component’s name, acnpg-objectstoreunder the same name (one same-name sibling group) whenobjectStoreis set, acnpg-pooler<name>-poolerwhenpooler.enabled, and acnpg-database<name>-<db>perdatabasesentry (a Database given the Pooler’s name, by default the one namedpoolerbeside an enabled pooler, joins its same-name sibling group, as two objects of different kinds).replicasandstorageSizeare written to the Cluster only when authored, so the policy applies to them exactly as before. The two values postgresql derived from the policy are set after it by a post-policy step the rule attaches to the Cluster (oam.Component.AfterPolicy), which runs before the authored traits:enablePDB(instances > 1) and the1Gistorage fallback, under the policy maximum with postgresql’s text (storageSize "1Gi" exceeds enforced maximum "512Mi"). Authored traits go to the Cluster, and those that covered every object postgresql generated are forwarded to the other members so they still do:prune-protectionandforce-replaceto each member, andfluxcd-patchesandfluxcd-postbuild(not built in; a consumer that delivers through Flux registers them) to the first member that is ordered after the Cluster (below): the members share one group, so one of them carries the trait for that group’s bundle. For that reason a document that authors one of these two traits on the postgresql component may not order or place a member on its own (adependencyrule or aplacementwhose component is the Pooler’s or a Database’s generated name): the member could leave the group the trait reaches, so the rule refuses it and asks for the postgresql component’s name instead (the rule for every lowering rule, and the message, are under “Traits a lowering rule forwards”). Policies that name the postgresql component are extended to the members the same way. Aplacementof it is repeated for each member, so they stay in the Cluster’s tier and, without a dependency policy, in its bundle. When the document orders its components with adependencypolicy that has a rule, the rule adds one making the Pooler and the Databases depend on the Cluster, and every component the document makes depend on the postgresql component depend on them too: the members are then in a later group than the Cluster’s. Without one it adds no edge, since a dependency edge would order a document that never asked for ordering. The policies it adds are named<name>-dependenciesand<name>-placement-<i>(one per member), or, when a policy of the document already uses the name, the first free one with-<n>appended. Behavior-changing underlauncher.gokure.dev/v1alpha1(go-kure/launcher#281): with bothobjectStoreandpooler, the objects now come in the order Cluster, ObjectStore, Pooler (was Cluster, Pooler, ObjectStore; the objects are the same), and a Database namedpoolercomes right after the Pooler, ahead of the other Databases. Under adependencypolicy the Pooler and the Databases are deployed from one bundle they share, after the Cluster’s, unless the document orders or places a generated member on its own (was one bundle for all of postgresql’s objects). A Pooler or Database name, generated or chosen, that is already the name of another component in the document is refused, naming both. A database name repeated indatabasesis refused, naming both entries: each entry is one Database object, so the Flux build already failed on it, and the plain manifest output carried duplicate Database documents (kubectl kept the last). The Cluster kind’s policy checks now apply, under its field names: the registry allowlist on the image (imageName, derived or authored), andinstances/storage.sizein the policy messages. Refusals the API server or the operator gave at apply now come at build: a resource request above its limit, a database namedpostgres,template0,template1or not a DNS-1123 subdomain, and an authoredstorageSize: ""(storageSize: must not be empty; omit it to take the policy default or 1Gi), which built a Cluster CloudNativePG’s webhook refuses (“Size not configured”). The component name becomes the Cluster’s name and itscnpg.io/clusterendpoint selector unless the Cluster is named apart (clusterObjectNameor theNaminghook, below), so it carries cnpg-cluster’s name rule: a DNS-1035 label (no leading digit, no dot) of at most 50 characters. Any other name is refused when the rule lowers it and when endpoints are collected (postgresql name "db.main": must be a DNS-1035 label of at most 50 characters …). The cap also keeps both endpoint selector values, the name and the pooler’s default<name>-pooler, within the 63-character label-value limit (apoolerNameor a hook-given name is held to it as a DNS-1035 label, and so is the default where the Cluster is named apart and the component name is not bounded by the Cluster’s rule). Behavior-changing underlauncher.gokure.dev/v1alpha1: such a name used to build and was then refused by CloudNativePG at apply, or, over 63 characters (56 with the pooler), gave a network-policy selector the API server refuses; it is now refused at build (go-kure/launcher#615, go-kure/launcher#589). An authoredversion: ""is refused by name (version: must not be empty; omit it to default to "16"), with or withoutimageName: it used to be kept as a value, so withoutimageNamethe cluster image wasghcr.io/cloudnative-pg/postgresql:— an empty tag that only the image pull rejected (go-kure/launcher#539). Only an omitted or nullversiontakes the default.storageSize: ""is refused (above). The image is held to the tag rule (ValidateImageRef), with or without a policy (breaking, go-kure/launcher#790). WithoutimageNamethe image isghcr.io/cloudnative-pg/postgresql:<version>, so aversionthat makes a:latestreference, or none that parses, is refused under the property the author wrote, with the image it made:version: "latest" is refused as the tag of the default image: image "ghcr.io/cloudnative-pg/postgresql:latest" rejected: :latest tag not allowed; use an explicit version tag or digest. An authoredimageNameis refused underimageName;versionis read for the default image only and is not checked beside one. The defaultversionbuilds.pooler.imageis the PgBouncer image, written to the Pooler’spgbouncer.imageand held to the tag rule under its own name (pooler.image: image "…" rejected: …) and, by thecnpg-poolerthe pooler lowers to, to the registry allowlist. Like the other pooler settings it needspooler.enabled. Without it the CloudNativePG operator chooses the PgBouncer image, so under a non-empty allowlist an enabled pooler withoutpooler.imageis refused (breaking, go-kure/launcher#790: it built before), reported under the Pooler’s component name:pgbouncer.image (pooler.image on a postgresql component): unset, so the CloudNativePG operator chooses the image the pods run, which the allowed registries [...] cannot hold; name an image from one of them. Setpooler.imageto an image from the list. With no allowlist nothing changes. Any other storage size the Cluster would carry, authored or from the policy default, must parse and be positive (storageSize: quantity must be positive, got "0"): CloudNativePG’s webhook parses only the size, so a zero or negative one was admitted and its claims failed the API server’s positive storage-request check. A policy storage-size default is checked when it is applied (policy default for storage.size: invalid quantity "lots"), as the cpu/memory defaults are. The instance count must be at least 1, the CRD’s minimum: a replicas policy default of 0 is refused (instances: must be >= 1, got 0 from the policy default), and so is an authoredreplicas: 0when the rule lowers it (replicas: must be >= 1, got 0). Behavior-changing underlauncher.gokure.dev/v1alpha1: an authoredreplicas: 0or a non-positivestorageSizeused to build and was then refused at apply; it is now refused at build (go-kure/launcher#623).resourcesforwards every name the shared parser admits —cpu,memory,ephemeral-storage,hugepages-<size>and qualified extended resources such asnvidia.com/gpu, under the same validation as the other seven workload kinds — onto the Cluster’sspec.resourcesrequests/limitsunchanged (go-kure/launcher#484; until then any name other thancpu/memorywas rejected, so this is an additive change). Onlycpu/memorycarry a policy default and maximum; CNPG-specific sizing such asephemeralVolumesSizeLimitis not derived from these entries. CNPG copies this block onto the instance pods unchanged and nothing defaults cpu/memory into it except a policy, so generation refuses a block that nameshugepages-<size>withoutcpuormemoryon either side after policy defaults (resources: hugepages require cpu or memory in requests or limits) — admission would refuse every instance pod.affinitytakes the same four keys as the sharedaffinityproperty (enablePodAntiAffinity,topologyKey,podAntiAffinityType,nodeSelector) with the same defaults —kubernetes.io/hostnameandpreferred— but the handler parses it itself into the CNPGAffinityConfigurationinstead of callingparseAffinity, because CNPG carries its own affinity shape rather than acorev1.Affinity. The block and each of its four sub-fields are read with the presence-reporting helpers, so a value authored with the wrong type is rejected by name (affinity.topologyKey: must be a string, got float64) rather than discarded while a default reaches the emitted cluster, andpodAntiAffinityTypemust bepreferredorrequired— an explicit empty string is an error here, not a fallback to the default (go-kure/launcher#448). Rejection reaches thenodeSelectorvalues, not just its envelope:nodeSelector: {rack: 3}is refused by key rather than dropped, so a selector cannot silently reach the cluster narrower than authored. When more than one entry is wrong, the alphabetically first offending key is the one reported: Go randomises map iteration order, so without an explicit sort the same document names a different key run to run and its diagnostic cannot be reproduced or asserted in a test. The same ordering rule applies to the other three parsers in this package that report a bad map entry by key —parseResourceList,rejectUnknownKeysandparseManifestSource. An explicit nullaffinityis absence — Launcher supplies no explicit affinity settings, exactly as when the property is omitted — matching whatpkg/oam’s own property validation already does with a null under an optional property; without that,affinity:with no value validated against the published schema and then failed to convert, while the same value built in Go turned pod anti-affinity on with every default. A null on a sub-field inside a present block is also absence — it takes the same default an omitted sub-field takes (go-kure/launcher#444 established this nested-null contract in the shared presence primitiveauthoredValue, which this block’s four sub-fields go through; a nested key read raw elsewhere, such as a probe handler key, does not follow it); a non-null wrong-typed sub-field, and an explicitly emptypodAntiAffinityType, remain errors. The sharedparseAffinity(common.go) applies the same rules to its four sub-fields since go-kure/launcher#452, so the two readers agree; a change to one belongs in the other. The handler’s other string→string maps —inheritedMetadata.labels,inheritedMetadata.annotations,postgresql.parameters,pooler.parametersandexternalClusters[].connectionParameters— take string values only: a non-string value (tier: 3,max_connections: 100,enabled: true) is refused by field and key (postgresql.parameters["max_connections"]: must be a string, got int) instead of being dropped from the emitted resources, with the same alphabetically-first rule asnodeSelectorwhen several are wrong (go-kure/launcher#466). Quote such a value to keep it (max_connections: "100"), and declare a Package parameter substituted as a whole value there astype: string, since whole-value substitution keeps the parameter’s type; a number is not converted to its string form, because YAML’s rendering of a number is not always the text the author typed (1.10decodes to1.1). An explicit null value is absence, per “The null contract” below: the key is left out, not refused. The same holds fornodeSelectorvalues in both affinity readers, which share the helper. AnobjectStoreblock emits a barman-cloudObjectStorenamed after the component (or byobjectStoreObjectNameor theNaminghook, below) and a WAL-archiver entry on the Cluster for the pluginbarman-cloud.cloudnative-pg.io, whosebarmanObjectNameparameter names that store by the name it took. An authoredobjectStore.serverNamebecomes that entry’sserverNameparameter, where the plugin reads it (it defaults to the Cluster name); the ObjectStore CRD forbids aconfiguration.serverName. Until go-kure/launcher#643 the entry named a plugin CloudNativePG does not register (barman-cloud.barmancloud.cnpg.io) under a key the plugin does not read (objectStoreName), andserverNamewent on the ObjectStore, which the API server refused. No document is newly refused; only the emitted output changes. Every other optional property the handler reads goes through the same presence-reporting helpers (go-kure/launcher#512):provider,version,storageSize,imageName, thebackup,monitoring,pooler,bootstrap,replication.synchronousandobjectStoreblocks and their fields, and each entry ofmanagedRoles,databases(with itsextensions),externalClustersandmonitoring.customQueries. A value of the wrong type is refused by its path (managedRoles[0].login: must be a boolean, got string,databases[0].extensions[1]: must be an object, got string) where it used to be dropped, sopooler.enabled: "true"no longer builds a cluster with no pooler andbackup: "s3"no longer builds one with no backup; an explicit null is absence, typed or untyped, so a typed-nilstorageSizetakes the policy default and a typed-nilpooler.instancesorreplication.synchronous.numbertakes the handler default rather than being refused as a bad number. Two further shapes that used to vanish are now errors: a non-stringmanagedRoles[].inRolesentry, and anexternalClustersentry without aname. So is a managed role’sconnectionLimit: 0(go-kure/launcher#659), ascnpg-clusterrefuses it: CloudNativePG omits a zeroconnectionLimitfrom the Cluster and applies its default-1, so the role would deploy with no limit. A role that must not connect setslogin: falseinstead. The lowering repeats the refusal for aPostgresqlConfigbuilt directly rather than parsed.Endpointsruns the rule’s lowering on the component, so what the lowering refuses of it has no endpoint, in the same words. What a kind it lowers into refuses of a member (a database namedpostgres) is not seen there: one of the four things an endpoint entry does not see (pkg/oam, “Name roles and theNaminghook”). Two object store paths are required (go-kure/launcher#790):backup.destinationPathwhere another value ofbackupis set (retentionPolicy,endpointURLorsecretName), andbarmanObjectStore.destinationPathof anexternalClustersentry that has abarmanObjectStore(backup.destinationPath: required (…),externalClusters[0].barmanObjectStore.destinationPath: required (…)). The Cluster’s CRD requires both. Under aretentionPolicythat is not empty, and in an external cluster’sbarmanObjectStore, the lowering used to write""where the author wrote nothing, which the API server refuses. AnendpointURL, asecretNameor an emptyretentionPolicywithout the path used to build no backup at all, and is refused as a block that would not be built (below). An authored empty path is a value: it is written, and refusing it is left to the API server. These are two of the stringscnpg-clusterrefuses unauthored; the lowering fills the parent of two more,bootstrap.pg_basebackup.sourceand the Cluster’spostgresql.synchronous.method(authored here asreplication.synchronous.method), only from the string itself, and of the other seven never.TestPostgresqlRule_UnauthoredRequiredStringsreads all eleven in the lowered component. An external cluster’sbarmanObjectStoreis decoded into the upstream type, which reads a key in any spelling, so its path counts as authored in any spelling. The refusal is made on the authored properties, so aPostgresqlConfigbuilt in Go is not held to it. A block that would not be built is refused, not dropped (go-kure/launcher#790). Five blocks are built only when one field of theirs is set, and a document that authored other values of such a block without that field used to build with none of them in the result. It is refused by the name of the missing field:backup: the Cluster’s backup is built fromdestinationPath, soendpointURL,secretNameor an emptyretentionPolicywithout one is refused, as aretentionPolicythat is not empty already was (backup.destinationPath: required (…), above). An authored empty path builds the backup and is written, also without a retention policy.bootstrap.recoveryandbootstrap.pg_basebackup: the bootstrap is built from a non-emptysource, so either block without one is refused (bootstrap.pg_basebackup.source: required (…), ormust not be empty (…)for an authored"").replication.synchronous: built frommethod, so the block without one is refused (replication.synchronous.method: required (any or first)), after a wrongly typednumberordataDurability, which is still refused by its own path.monitoring: built whenenabledis true, socustomQueriesbeside anenabledthat is not authored is refused (monitoring.enabled: required where monitoring.customQueries is set (…)).pooler: the Pooler is emitted whenenabledis true, soinstances,type,poolMode,parametersorimagebeside anenabledthat is not authored is refused (pooler.enabled: required where pooler.instances is set (…)).
An authored
enabled: falsekeeps building, inpoolerand inmonitoring:enabledis the block’s own switch, the author wrote “off”, and the settings beside it are kept in the document for when it is switched on.poolerNamediffers (refused wheneverpooler.enabledis not true, an authored false included) because it is a property outside the block that names an object which does not exist while the pooler is off. A block with nothing in it (backup: {},bootstrap: {},replication: {},monitoring: {},pooler: {}), or with an emptycustomQuerieslist orparametersmap only, authors no value and builds as before.TestPostgresqlRule_BlockNotBuiltholds each case. Breaking for a document that authored one of these blocks without its field, among them abackupwith anendpointURLor an emptyretentionPolicyand no path, abootstrap.pg_basebackup: {}and areplication.synchronouswith anumberonly. Such a document built, without what the author wrote. An authoredpooler.instancesis written as authored (go-kure/launcher#790), a 0 or a negative count included. The Pooler CRD givesinstancesa default of 1 and no minimum, and a count of 0 or less used to be left out of the Pooler, so the operator ran the one pod the author had switched off. The upstream type carries a 0, so it is written; the CRD does not bound the count, so a negative one is written too and refusing it is left to the cluster. A count that is not authored is still 3 (TestPostgresqlRule_PoolerInstances). Behavior-changing for a document withpooler.instances: 0or less: its Pooler now holds that count. A document withinstances: 0got one pod before and gets none now (thepostgresql-bootstrap-externalfixture shows it). The Pooler’s and the Databases’ names (go-kure/launcher#787) are resolved in the order of every name role (see “Name roles and theNaminghook” inpkg/oam/README.md): the author’s property, else the consumer’sNaminghook, else the default.poolerNamenames the Pooler (rolepooler, default<name>-pooler) and must be a DNS-1035 label, since CloudNativePG names the pooler’s Service after it; it is refused whenpooler.enabledis not true.databases[].objectNamenames that entry’s Database object (roledatabase, default<name>-<db>) and must be a DNS-1123 subdomain; the entry’snamestays the database created in PostgreSQL. A database whosenamecannot be part of an object name (app_data) has no default and needs anobjectName: without one it is refused, and the hook is not asked for it. A name is used as written or refused, never shortened. Each is also the name of the component the rule emits, so one that is already a component of the document, the postgresql component’s own name included, is refused, naming both; two Databases given one name are refused, naming both entries; a Database given the Pooler’s name is its sibling, as above. The hook is asked only insideTransform: a rule driven directly keeps the author’s names and the defaults. With neither an authored name nor a hook the output is what it was. The Cluster’s and the ObjectStore’s names (go-kure/launcher#787) are resolved in the same order.clusterObjectNamenames the Cluster (rolepostgresql-cluster) andobjectStoreObjectNamethe ObjectStore (rolepostgresql-objectstore); the default of each is the component name. The two end inObjectNameasobjectNamedoes on a kind component, whilepoolerName, the older property, keeps the name it has.objectStoreObjectNameis refused withoutobjectStore(objectStoreObjectName: names the ObjectStore, and objectStore is not set; remove it, or set objectStore), and the hook is not asked for a store the component does not generate. The Cluster’s name is held to the Cluster’s rule whoever chose it, a DNS-1035 label of at most 50 characters: a name that is no label is refused as every role’s is, and a label over 50 characters by the rule, naming both places it can come from (clusterObjectName (or the Naming hook's answer for role "postgresql-cluster"): postgresql Cluster name "…": must be a DNS-1035 label of at most 50 characters …). A component whose own name is no such label is accepted once its Cluster is named apart. The ObjectStore’s name is a DNS-1123 subdomain. Each is used as written or refused, never shortened. Each names its object alone: the two member components, their same-name sibling group, the policies the rule adds and the component label keep the component name. What the rule writes to the two objects follows the names they took: the Pooler’scluster.name, each Database’scluster.name, thecnpg.io/clusterendpoint selector, and thebarmanObjectNameparameter of the Cluster’s backup plugin. The default names of the Pooler and the Databases keep deriving from the component name (<name>-pooler,<name>-<db>), whatever the Cluster is named. With the Cluster named apart the component name is no longer bounded by the Cluster’s rule, so the Pooler’s default is held to the Pooler’s own, by the lowering and where endpoints are collected alike, and the refusal says what settles it (pooler: cnpg-pooler name "db.main-pooler": must be a DNS-1035 label of at most 63 characters (CloudNativePG names the pooler's Service after it); the default derives from the component name: set poolerName to name the Pooler otherwise). A Pooler cannot carry its Cluster’s name, and with the Cluster named apart the two can meet (aclusterObjectNameofdb-pooleron componentdb): it is refused where the Pooler is built, and where endpoints are collected in the same words (cluster.name "db-pooler": a pooler cannot have the same name as its cluster). A Pooler or a Database named like thepostgresqlcomponent itself (apoolerNameofdbon componentdb, a database’sobjectName, or the hook’s answer) is a name the document already holds: it is refused by the lowering, and where endpoints are collected in the same words (pooler: generates component "db", which is already the name of component "db" (type "postgresql") in the document; rename one of them). One named like another component of the document is refused by the lowering only: endpoints are collected for one component (pkg/oam, “Name roles and theNaminghook”). CloudNativePG derives the Cluster’s Services (<cluster>-rw,<cluster>-ro,<cluster>-r) and Secrets (<cluster>-appand the others) from the Cluster’s name, and the default backup path moves with it: the server name a backup is stored under is the Cluster’s name unlessobjectStore.serverNamesets it. Renaming an existing Cluster creates a new one: the old one is pruned with its data unless it is protected. Every address or Secret reference written with the old name is the author’s to change. Its rule implements the optionaloam.EndpointProvider: it declares the CNPG cluster’s data-plane endpoint (cnpg.io/cluster: <Cluster name>, the component name unless the Cluster is named apart, on port5432) so a downstream platform can synthesize the target-side ingress allow ({comp}-allow-endpoint-ingress) without hardcoding the operator selector. Whenpooler.enabledis set it declares a second endpoint for the pooler (PgBouncer) pods (cnpg.io/poolerName: <pooler name>on port5432), so a consumer that dials the pooler — whose pods carry a different label set and are not matched by the direct-cluster selector — also gets its connection synthesized. The pooler name there is the authoredpoolerName, else<component-name>-pooler, and the Cluster name the authoredclusterObjectName, else the component name:Transformer.ComponentEndpointsasks no naming hook. The rule also implementsoam.NamedEndpointProvider, through whichTransformer.ComponentEndpointsNamedresolves each name with the consumer’s hook, asked the request the transform asks, so a consumer that names the Pooler or the Cluster through the hook gets a selector for the name the object has.cnpg-cluster — the operator-CR kind component for a CloudNativePG
Cluster(design:docs/oam/design-operator-cr-components.md). The component name becomes the Cluster’s name and itscnpg.io/clusterendpoint selector, so it must be what CloudNativePG’s admission webhook admits: a DNS-1035 label (no leading digit, no dot) of at most 50 characters. Any other name is refused at parse time, when endpoints are collected and byGenerate(cnpg-cluster name "db.main": must be a DNS-1035 label of at most 50 characters …), although other kinds accept DNS-1123 subdomains of up to 253. Its properties are thespecof apostgresql.cnpg.io/v1Cluster, one schema key perClusterSpecjson field: scalars are typed, and every structured field is an openobjector anarrayof open objects whose description points at the upstream type.ToApplicationConfigdecodes the whole property map intocnpgv1.ClusterSpecwithbuiltin.DecodeStrictJSON, so a key the linked CNPG version does not declare, at any depth (storage.sise,bootstrap.initdb.databse), and a value of the wrong type (storage.size: 5) are refused rather than dropped. Two limits come fromencoding/jsonitself: the unknown-field error names the key but not its path, and a key differing from a declared one only in letter case is accepted as that field (two spellings of one field in the same object are refused, since the decoder would keep only one:storage.size: sets the same field as storage.Size …). The policy defaults below read what was authored from the decoded spec as well as from the property map, where every spelling the decoder accepts is consulted, so such a key still counts as authored and is not overwritten by a default — including an emptystorage.size, which the decoded spec cannot tell from absence. It carries no launcher opinions:Generateemits the Cluster named after the component in the build namespace with exactly the authored spec, so an unauthored field is left for the operator’s own default.postgresql’s choices — an image fromversion,primaryUpdateStrategy: unsupervised,enablePDBfrom the replica count, a1Gistorage fallback, pod anti-affinity — are not made here: its lowering rule writes them as this kind’s properties, and the trait it attaches sets the two that depend on the policy (seepostgresql). The one value it writes unasked isinstances: 1, the CRD default, becauseClusterSpec.Instanceshas noomitemptyand would otherwise serialize as0. The non-pointer blocks appear even when unauthored, as they do forpostgresql:affinityandresourcesas empty objects, andpostgresqlassyncReplicaElectionConstraint: {enabled: false}, because that nested field’senabledhas noomitempty. Nulls follow “The null contract” below, and a null is whatever serializes to JSON null: the properties are marshalled withencoding/jsonand read back before anything else looks at them, so a null map value is left out rather than decoded to an empty string, and a null array element is refused by path (env[0]: null is not a valid array element). The contract covers declared keys only: a key the CNPG type does not declare is refused as an unknown field even when its value is null (storage: {sise: null}), not dropped with the null. A lowering rule emits JSON-shaped values (string-keyed maps, slices, scalars) as for every component; that is the supported contract, and the engine’s schema check refuses a typed API struct where it checks an object or array item (for example anenventry), though not inside an open object’s undeclared keys. Because the reading is the serialization itself, a direct caller of the handler additionally has a value built in Go read exactly asencoding/jsonwrites it — a concrete collection type, a nil behind a pointer, ajson.RawMessageand a custom encoder included. A value that does not serialize is refused. Aninstancesbelow 1, in any spelling, is refused before policy runs (instances: must be >= 1, got 0): the CRD’s minimum is 1, so the API server would refuse the Cluster. A policy instance-count default below 1 is refused the same way.Generaterepeats the name and instance-count refusals on what it emits, the count’s upper bound (instances: must be <= 2147483647, got …) included, so aCnpgClusterConfigbuilt in Go withoutToApplicationConfigcannot produce a Cluster the operator or the API server rejects for either. It also refuses any storage request the Cluster carries that does not parse or is not positive, authored or from a policy default, on every claim the storage maximum below covers (storage.size: quantity must be positive, got "0";walStorage.pvcTemplate.resources.requests.storage: …): CloudNativePG’s webhook parses onlystorage.size, so such a Cluster was admitted and its claims then failed the API server’s positive storage-request check (go-kure/launcher#623). An unset size is still left to the operator. An authored0orfalsethat the typed spec cannot carry, on a field whose CRD default is not zero, is refused by path (managed.roles[0].connectionLimit: 0 cannot be carried by the CloudNativePG API types (the field is omitted when zero, so the operator would apply its default -1)). These fields are non-pointer andomitempty, so the value is omitted when the Cluster is encoded and the operator applies its CRD default instead. For the linked CNPG version they aremanaged.roles[].connectionLimit(default-1, unlimited),postgresUIDandpostgresGID(26),startDelay(3600),stopDelay(1800),switchoverDelay(3600),probes.liveness.isolationCheck.requestTimeoutand.connectionTimeout(1000) andreplicationSlots.updateInterval(30); a test derives the set from the CNPG module’s CRD and Go types, so a CNPG bump that changes it fails CI. An explicit zero default on any other such field, such asmanaged.roles[].login: false,superuser: false,minSyncReplicas: 0ormonitoring.enablePodMonitor: false, is accepted: it is omitted too, and the field’s absence means the same value. Two fields the Cluster CRD requires, which the Go type leaves out when they are empty, are refused when unauthored or empty: thesignerNameand thekeyTypeof a pod certificate source of the projected volume template (projectedVolumeTemplate.sources[0].podCertificate.signerName: required (…)). The object would show the omission and the API server refuse it. Two more that the CRD requires, which the Go type writes asnullwhen they are unauthored, are refused when unauthored: thenodeSelectorTermsof a required node affinity (affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms: required (…)) and thedatabasesof an import (bootstrap.initdb.import.databases: required (…)). The API server drops a null of a field that is not nullable before it validates, so the required field is then missing. An authored empty list is written as one and is not refused here. A third such field is not refused:replicationSlots.synchronizeReplicas.enabled, which the CRD defaults, so the API server puts the default (true) in the place of the null.TestKindComponents_NullRequiredderives the three from the CRD and shows each answer by running the CRD’s schema validator on the object. Eleven strings the CRD requires and bounds, with a minimum length or an enumeration, which the Go type writes as""when they are unauthored, are refused when unauthored, each where its parent is authored (replica.source: required (…)):backup.barmanObjectStore.destinationPath,bootstrap.initdb.import.type(microserviceormonolith),bootstrap.pg_basebackup.source,externalClusters[].barmanObjectStore.destinationPath,managed.services.additional[].selectorType(rw,rorro),podSelectorRefs[].name, thenameof apostgresql.extensions[]entry and thenameand thevalueof itsenv[]entries,postgresql.synchronous.method(anyorfirst) andreplica.source. The Cluster would carry an empty value the author did not write, and the API server refuse it. An authored empty one is a value: it is written, and refusing it is left to the API server. The refusal reads the authored properties, soGeneratedoes not repeat it: aCnpgClusterConfigbuilt in Go is not held to it. Nor is the component apostgresqlcomponent is lowered to, whose properties are written from a typed spec, so that a string left out there reaches the kind as an authored"":postgresqlrefuses the two it could leave out itself, under the property its author wrote (above). The same test derives every field the type writes unauthored as a zero value the CRD’s own rule refuses, and shows each answer with the validator; the twelfth of a Cluster isinstances, which is not refused (below).TestKindComponents_OmittedRequiredAndWrittenDefaultsderives the two from the CRD and shows the refusals. It also holds the two fields the CRD defaults and the type writes unauthored to a reason they are harmless:instances, of which the kind always writes a count (the authored one, the policy’s default, or the CRD’s own1), andbackup.volumeSnapshot.onlineConfiguration, written{}under an authoredvolumeSnapshot. The CRD’s default for the object iswaitForArchive: trueandimmediateCheckpoint: false; in a{}it fillswaitForArchivewith the field’s own defaulttrue, and an absentimmediateCheckpointisfalse. Afalseor0the type keeps (a pointer such asenablePDB: false, a quantitycpu: 0) is emitted as authored. An authored empty string the type omits on a field the CRD defaults (primaryUpdateStrategy: "",logLevel: "") is refused as a defaulted0is (cnpgClusterDefaultedZeroFields); elsewhere it is not refused —storage.size: "", which the CRD does not default, keeps its meaning above. The two pod-certificate fields above are refused too: an emptysignerNameorkeyTypeis refused as an unauthored one is. A match expression authored in one of the spec’s label selectors without itskeyor itsoperatoris refused by path (podSelectorRefs[0].selector.matchExpressions[0].operator: required (…)): the CRD requires both, and the type would write them empty. Presence only; “A label selector’s match expressions” lists the selectors and says what is left to the API server and to the operator’s webhook.ApplyPolicyenforces the policypostgresqlenforces (postgresql lowers onto this kind, so it is the same code), in this order: the instance-count default wheninstancesis not authored (an authored value wins even when it equals the fallback) and its maximum; the cpu and memory request and limit defaults onresources, filling only unset entries, and their maxima; and the storage-size default onstorage.sizewhen neitherstorage.sizenorstorage.pvcTemplate’s storage request is authored. With no policy default the size is left to the operator; the1Gifallback is postgresql’s, applied after the policy by the post-policy step its rule attaches (ApplyPostgresqlDefaults: the fallback is held to the policy maximum, andenablePDBis set from the instance count; a fallback over the maximum is the component’soam.ViolationErrorof classoam.RefusalStorageMaximum, as a refusal ofApplyPolicyis). That method is for that step, not an authoring surface. A storage-size default that is not a quantity is refused (policy default for storage.size: invalid quantity "lots"), as an invalid cpu or memory default is, since with no maximum set nothing else would parse it. So is a zero or negative one (quantity must be positive, got "0"): CloudNativePG’s webhook only parses the size, and the claims it creates would then fail the API server’s positive storage-request check. The storage maximum applies to every claim the Cluster creates —storage,walStorage, eachtablespaces[i].storage(eithersizeor thepvcTemplaterequest) andephemeralVolumeSource.volumeClaimTemplate— and the error names the one that exceeds it. Because this kind exposes the two security contexts, it also refusessecurityContext.privilegedand asecurityContextorpodSecurityContextwindowsOptions.hostProcessunless the policy allows privileged workloads, and an added capability the policy forbids or leaves off a non-empty allowlist. The registry allowlist the workload kinds apply to their image applies to an authoredimageName(imageName: image "…" is not from an allowed registry [...]). WhenimageNameis unset, the operator takes the image from the catalogimageCatalogRefnames, which this kind does not check, or, without one, chooses an image of its own, which no allowlist can hold: under a non-empty allowlist a Cluster with neitherimageNamenorimageCatalogRefis refused, with the registry class (breaking, go-kure/launcher#790: it built before):imageName: unset, so the CloudNativePG operator chooses the image the pods run, which the allowed registries [...] cannot hold; name an image from one of them. SetimageName, orimageCatalogRefto take the image from a catalog. With no allowlist nothing changes. The operator’s own image, which it runs in the instance pods’ bootstrap init container, is not covered: no field of the Cluster names it. Apostgresqlcomponent always writesimageName(its derivedghcr.io/cloudnative-pg/postgresql:<version>or the authored one), so the allowlist applies to its image too. The allowlist applies as well to the image of eachpostgresql.extensions[]entry, which the operator mounts into the instance pods as an image volume (breaking, go-kure/launcher#790: an extension image from a registry outside the list built before):postgresql.extensions[0].image.reference: image "…" is not from an allowed registry [...], with the registry class, the reference read asimageNameis (see Image volumes under pod). An entry without a reference is not checked: the operator takes that image from the catalogimageCatalogRefnames (and refuses the Cluster without one), and a catalog’s images are not checked by this kind, as for an unsetimageName. A build reads the objects a document brings, not an object a reference points at: a catalog’s images are held where the catalog is authored, by the kind that builds it.cnpg-imagecatalogandcnpg-clusterimagecataloghold every image of a catalog they build to the same list (see their entry above). Nothing in this library holds the images of any other catalog: not of a raw catalog onmanifests,passthroughor a chart, and not of one that already exists in the cluster. Apostgresqlcomponent writes no extension entry.imageNameand the extension images are every image a Cluster names today; the test that walks the pod spec for image fields walks the Cluster and the Pooler spec the same way. Both are also held to the tag rule (ValidateImageRef) when the component is read and again at generation, with or without a policy (breaking, go-kure/launcher#790: an untagged or:latestimageNameor extension reference built before):imageName: image "…" rejected: no tag or digest specified; use an explicit version tag or digest,postgresql.extensions[0].image.reference: image "…" rejected: …. An unsetimageNameand an entry without a reference name no image and are not checked, as for the allowlist. A digest without a tag passes, here as everywhere the rule runs; onimageNameCloudNativePG’s webhook refuses one, and a tag that is no PostgreSQL version, and those are the operator’s value rules, left to the operator. Generation refuseshugepages-<size>inresourceswithoutcpuormemoryafter policy defaults. It also applies the shared parser’s request/limit cross-check there (resources: cpu: request 2 must not exceed limit 1; hugepages and extended resources need a limit equal to the request), so a policy default limit below an authored request is refused as well. The other resource-name rules of the shared parser are left to the API server.Endpointsdeclares the same primary endpoint aspostgresql(cnpg.io/cluster: <Cluster name>on port5432), and only for a component the parse accepts: whatToApplicationConfigrefuses of the name or of the properties is refused where endpoints are collected, in the same words (instances: must be >= 1, got 0). WhatGeneraterefuses once the policy is applied is not repeated there.TestCnpgClusterSchema_CoversClusterSpecpins the schema toClusterSpecby reflection: each json field is published with its type or listed with a reason incnpgClusterExcludedFields(empty today), and a schema key with no field or a stale exclusion also fails, so a CNPG bump that adds, removes or retypes a field names it.cnpg-pooler, cnpg-database, cnpg-objectstore — the operator-CR kind components for a CloudNativePG
PoolerandDatabaseand a Barman Cloud pluginObjectStore(barmancloud.cnpg.io/v1), built on thecnpg-clusterrecipe: one schema key per json field ofPoolerSpec,DatabaseSpecorObjectStoreSpec, the whole property map decoded strictly into that type under the same null contract, the same refusal of an authored0orfalsethe type cannot carry (for the linked versions onlyinstanceSidecarConfiguration.retentionPolicyIntervalSeconds, default1800, on the ObjectStore) and of two spellings of one field, and the same reflection tests pinning the schema and both derived lists to the linked modules.Generateemits one object named after the component in the build namespace with the authored spec, and repeats the parse-time refusals made on the decoded spec. The fields each CRD requires are refused when unauthored or empty, by path:cluster.nameandpgbounceron the Pooler (pgbouncer: {}selects PgBouncer’s defaults);cluster.name,nameandowneron the Database;configuration.destinationPathon the ObjectStore. The published schema marks the top-level property of each as required (go-kure/launcher#790):clusterandpgbouncerof acnpg-pooler;cluster,nameandownerof acnpg-database;configurationof acnpg-objectstore. That refuses no component that built before, since the kind refused each without it already. Acluster.namemust be a name CloudNativePG admits for a Cluster (a DNS-1035 label of at most 50 characters). On the Pooler, a match expression authored in a label selector oftemplate.specwithout itskeyor itsoperatoris refused too, by path; an authored empty one is not (presence only; see “A label selector’s match expressions”).cnpg-poolerwrites no type or instance count of its own, so the operator’s defaults (rw,1) apply. Its component name is the Pooler’s name and its Service’s, so it must be a DNS-1035 label of at most 63 characters, and a pooler named like its cluster is refused, as CloudNativePG’s webhook does: by the build, and where its endpoint is collected in the same words (cluster.name "main": a pooler cannot have the same name as its cluster).ApplyPolicyapplies the workload kinds’ pod gates totemplate.spec(host namespaces, hostPath volumes, privilege, host-process, capabilities, the registry allowlist on each authored container image, cpu and memory maxima; errors name the container, as intemplate.spec.containers[0] "pgbouncer": cpu limit "2" exceeds enforced maximum "1"), caps a generic ephemeral volume’s claim at the policy storage maximum, holds an image volume’s reference to the registry allowlist (template.spec: volume "ext" image.reference: image "…" is not from an allowed registry [...]; breaking, go-kure/launcher#790, see Image volumes under pod), and applies the registry allowlist to an authoredpgbouncer.image. With or without a policy, when the component is read and again at generation, an authoredpgbouncer.image, the image of each init and regular container of the template and the reference of each of its image volumes are held to the tag rule (ValidateImageRef; breaking, go-kure/launcher#790: an untagged or:latestone built before):pgbouncer.image: image "…" rejected: …,template.spec.containers[0] "pgbouncer": image "…" rejected: …. A template container that names no image is not checked: the operator supplies the PgBouncer image, so that is the ordinary form here, where the pod kinds refuse it. The operator takes the PgBouncer image, in order, from the template’s regular container namedpgbouncer, frompgbouncer.image, from the catalogpgbouncer.imageCatalogRefnames (not checked by this kind, as forcnpg-cluster), and otherwise chooses one of its own, which no allowlist can hold. Under a non-empty allowlist a Pooler that names its image none of these three ways is refused, with the registry class (breaking, go-kure/launcher#790: it built before):pgbouncer.image (pooler.image on a postgresql component): unset, so the CloudNativePG operator chooses the image the pods run, which the allowed registries [...] cannot hold; name an image from one of them. An image on an init container or on a container of another name does not count. With no allowlist nothing changes. The operator’s own image, which it runs in the pod’s bootstrap init container, is not covered: no field of the Pooler names it. Generation runs the shared parser’s request/limit and hugepages checks on the template’s pod and container resources (template.spec.containers[0] "pgbouncer": resources: cpu: request 2 must not exceed limit 1), ascnpg-clusterdoes on itsresources; the other resource-name rules of the shared parser are left to the API server. A template declaringephemeralContainers,activeDeadlineSeconds,priorityoroverheadis refused, as the workload kinds refuse them: the operator copies the template into a Deployment, whose pod template cannot carry the first two and whose pods get the last two from the default Priority and RuntimeClass admission controllers, which refuse an authored value that differs from theirs. Four fields the Pooler CRD requires undertemplate.spec, which the pod spec’s type leaves out when they are empty, are refused when unauthored or empty: theactionof a restart rule of a container or an init container and theoperatorof its exit codes (template.spec.containers[0].restartPolicyRules[0].action: required (…)), and thesignerNameandkeyTypeof a pod certificate source of a projected volume (template.spec.volumes[0].projected.sources[0].podCertificate.signerName: required (…)). The object would show the omission and the API server refuse it; the CRD of the linked module settles that, which is what the built-in pod kinds lack.TestKindComponents_OmittedRequiredAndWrittenDefaultsderives the fields from the CRD and shows the refusals. Five more that the CRD requires undertemplate.spec, which the pod spec’s type writes asnullwhen they are unauthored, are refused when unauthored: thenodeSelectorTermsof a required node affinity, thepriorityof an eviction responder, themonitorsof acephfsor anrbdvolume, and thesecretRefof ascaleIOvolume (template.spec.volumes[0].cephfs.monitors: required (…)). The API server drops a null of a field that is not nullable before it validates, so the required field is then missing. An authored empty list is written as one and is not refused here.template.spec.containersis such a field too and is not refused: the kind writescontainers: []itself (below).TestKindComponents_NullRequiredderives these fields andpgbouncerfrom the CRD and shows each answer by running the CRD’s schema validator on the object. One string the CRD requires and bounds with a pattern, which the type writes as""when it is unauthored, is refused when unauthored under an authoredpgbouncer.imageCatalogRef: itskey(pgbouncer.imageCatalogRef.key: required (…)). An authored empty one is written, and refusing it is left to the API server. The refusal reads the authored properties, soGeneratedoes not repeat it: aCnpgPoolerConfigbuilt in Go is not held to it. The same test derives it, as the one field of a Pooler the type writes unauthored as a zero value the CRD’s own rule refuses. The instance count is deliberately not policed:postgresql, which lowers its pooler onto this kind, never applied a policy to the pooler’s count, so a maximum here would refuse a document that built before. Pod-level errors name the template’s own fields, as intemplate.spec: resources cpu limit "2" exceeds enforced maximum "1". Atemplatethat lists no containers (labels only, say) is written withcontainers: []: the Go type cannot omit the template’sspec, the CRD requirescontainersonce it is present, and the operator adds itspgbouncercontainer either way.Endpointsdeclares the PgBouncer pods (cnpg.io/poolerName: <Pooler object name>on port5432: the component name unlessobjectNameor the naming hook names the Pooler), byte-identical topostgresql’s pooler endpoint for a pooler of that name (<component-name>-poolerby default, of thepostgresqlcomponent), and only for a component the parse accepts: whatToApplicationConfigrefuses is refused where endpoints are collected, in the same words (pgbouncer: required (an empty object selects PgBouncer's defaults)).cnpg-databasealso refuses the names the CRD reserves (postgres,template0,template1). Theensureof each schema, extension, fdw and server has noomitemptybut a CRD default ofpresent, soGeneratewritespresentwhere it is unauthored, as the API server would have; a test derives that list from the CRD too. An authoredensure: ""there is refused by its path (schemas[0].ensure: "" is refused by the Database CRD, …) rather than written aspresent, since the CRD’s enum refuses it as written; an explicit null is absence and takes the default. A Database runs nothing, so it has no policy and no endpoint.cnpg-objectstorecaps the cpu and memory requests and limits of the plugin sidecar it adds to every instance pod (instanceSidecarConfiguration.resources) at the policy maxima, filling no default, and declares no endpoint; generation runs the same request/limit and hugepages checks on them, leaving the other resource-name rules to the API server. It refusesconfiguration.serverName, which the shared Barman type carries but the plugin’s CRD forbids on an ObjectStore: the server name is a plugin parameter of the Cluster that uses the store.passthrough —
object(full apiVersion/kind/metadata/spec),clusterScoped. Its config exposesComponentName() string(theoam.ComponentNamedinterface) so consumers can attribute the emitted resource to its owning OAM component.objectmust be a single object; a list is rejected.Generateemits the map verbatim as one resource and fills in metadata it finds missing:metadata.namedefaults to the component name when it is absent, empty, or not a string, and on a namespaced objectmetadata.namespacelikewise — so an inline non-empty string namespace survives untouched, while a non-string one is replaced rather than emitted.Scope (go-kure/launcher#794, item 13). Whether the object is namespaced is resolved with the precedence
manifestsgives ascopeOverridesentry, withclusterScopedin the entry’s place (resolveClusterScoped,passthrough.go):- A kind whose scope the Kubernetes API governs (a built-in kind, a
CustomResourceDefinition;isAPIGovernedScope): kure’s scope table decides. APriorityClass, aClusterRoleor aNamespacegets no namespace without the property. AclusterScopedthat contradicts the table is refused:trueon a namespaced built-in, an explicitfalseon a cluster-scoped one (passthrough component "batch-low": clusterScoped is false, but the Kubernetes API defines scheduling.k8s.io/v1 PriorityClass as cluster-scoped; remove clusterScoped). This is the one difference frommanifests, which ignores ascopeOverridesentry for such a kind: its list may name kinds the source does not hold, whileclusterScopedis a statement about the one object, so a wrong one is an error in the document and not a spare entry. - Any other kind: an authored
clusterScopeddecides,trueorfalse, also against the table. A custom resource kure registers as cluster-scoped that the target cluster serves namespaced takesclusterScoped: false. - Not authored (absent or
null): the table decides for a kind kure registers, so a cert-managerClusterIssuergets no namespace. A kind nothing knows is treated as namespaced, the default this component always had.manifestsfails closed on an unknown scope;passthroughdoes not, since every custom resource passed through without the property relies on that default.
No
CustomResourceDefinitionis consulted: the component holds one object. A cluster-scoped object, however it was resolved, gets no namespace default, and an inline namespace on it is refused with the reason named (passthrough component "ca": object.metadata.namespace must not be set ("other"): cert-manager.io/v1 ClusterIssuer is registered as cluster-scoped (set clusterScoped: false if the cluster serves it namespaced); the Kubernetes API rejects a namespace on a cluster-scoped object). That rejection is keyed on a non-empty string, so a non-stringnamespaceon a cluster-scoped object is neither rejected nor defaulted and reaches the output as authored. The resolution runs inGeneratetoo, on the object about to be emitted, so aPassthroughConfigbuilt directly is held to it; itsClusterScopedfield statestrueor nothing, since an explicitfalseexists only for an authored property.Breaking (pre-GA): an object of a cluster-scoped kind the table knows loses the application namespace it was stamped with when
clusterScopedwas not set;clusterScoped: trueon a built-in namespaced kind andclusterScoped: falseon a built-in cluster-scoped kind stop building; and an inline namespace on a cluster-scoped kind the table knows is refused without the property too, where it was emitted as authored.Lists.
Generateemits the map as one resource, so a list would arrive downstream as one named envelope whoseitemsnever see per-object label mutation, namespace stamping or ownership checks — while Flux unwraps it at apply time into N objects that do reach the cluster. One envelope bypasses every per-object rule at once, which is why the rejection lives here and not in each consumer. Declare one component per object. The check is apimachinery’s ownUnstructured.IsList—itemspresent and a sequence — never the kind name, so a typedConfigMapListis caught and a CRD whose kind merely ends inListwith noitemsstill compiles.This arm is deliberately stricter than Kustomize for a kind that does not end in
Listbut carries a top-levelitemsarray, such as{kind: Widget, items: [...]}. Kustomize’s build keeps that as one resource, because it consultsitemsonly on aList-suffixed kind. Flux’s kustomize-controller then decodes the build output with the sameIsListpredicate and no kind check, and applies the array’s members instead of the object: aWidgetwhoseitemsholds aConfigMapputs thatConfigMapon the cluster, in whatever namespace it names, and never applies theWidget. With scalar members the Flux apply fails outright. Either way it is not one object, so it is rejected.A second arm catches what
IsListstructurally cannot. It requiresitemsto be exactly a[]interface{}, so an authoreditems: null— an untyped nil — passed every check and then expanded to zero objects at apply time with no error at all; with pruning enabled an empty desired result also removes whatever the previous inventory held. That case is rejected on its own diagnostic (“expands to zero objects”), and it is keyed on a nullitemson a kind ending inListrather than a present one — a CRD may legitimately carry an object-valueditemsfield that must keep compiling, and an ordinary, non-Listkind may equally carry a null-valueditemsfield (a plausible spec-field collision) without being an envelope at all. Matches Kustomize’s owninlineAnyEmbeddedLists, which checks the kind suffix before ever consultingitems; Flux agrees, since a nullitemsis not a list toIsListand the object is applied as one.The validated object is deep-copied when the config is built, not aliased, so a caller that keeps mutating the map it passed in cannot change what was validated. The copy detaches nested maps and slices whatever their concrete type — the authored path only ever yields
map[string]anyand[]any, but a Go-assembled body can hold amap[string]stringor a[]string, and those used to alias straight through. A null survives as a null rather than becoming an empty{}/[]. Two things it deliberately does not do: it does not chase pointers (a*Locationinside atime.Time,resource.Quantity’s*inf.Dec), and it does not reject the shapes it cannot fully detach.The copy is not what makes the rejection binding, and this section used to claim it was.
Objectis an exported field, so a caller holding the config can assign a fresh map over it, and a struct literal or a JSON decode never runsToApplicationConfigat all — three routes reachingGeneratewith a body the arms never saw.Generatetherefore re-runs, on the map it is about to emit, every check that must hold of an emitted body: a non-emptyapiVersionandkind, and both list arms. Checking the bytes being emitted, rather than trusting a check that ran on some earlier map, is what closes those routes.Those checks live in one function precisely so the two call sites cannot disagree about what a valid body is. While they were split — the constructor checking identity,
Generatechecking only list shape — aPassthroughConfigholding an empty map passed both: non-nil, so it cleared the no-object guard, and carrying noitems, so it cleared both arms, leavingGenerateto emit a document consisting of nothing but the metadata it had just stamped on.Policy.
ApplyPolicyholds the object to the environment policy (go-kure/launcher#794), with the check template delivery runs on the objects a chart renders (helmtemplate’s Every emitted workload); the transform calls it withNoopPolicywhen no policy is passed. A Pod, PodTemplate, ReplicationController, Deployment, StatefulSet, DaemonSet, ReplicaSet, Job or CronJob is checked as an authored workload is: host namespaces, hostPath volumes, the cpu/memory maxima, the storage maximum on a generic ephemeral volume’s claim, the registry allowlist on an image volume’s reference (breaking, go-kure/launcher#790: an object with one from a registry outside the list built before; see Image volumes under pod), and for every init and regular container the registry allowlist, the privileged, HostProcess and capability gates, andValidateImageRef(no untagged image, no:latest), which holds the reference of an image volume too (breaking, go-kure/launcher#790: an object with an untagged or:latestone built before). Ephemeral containers are refused. The storage a PersistentVolumeClaim, or a StatefulSet’s claim template, requests is held to the storage maximum (MaxStorageSize); the replica count of a Deployment, StatefulSet, ReplicaSet or ReplicationController (one when the object sets none) and themaxReplicasof a HorizontalPodAutoscaler, in any API version, to the replica maximum (MaxReplicas). A PersistentVolume is held to what thepersistentvolumekind holds its own to: ahostPathorlocalsource needsAllowHostPathVolumes(), andspec.capacity.storageis held to the storage maximum (breaking for a document that holds one; see persistentvolume). The error is the component’s policy violation and names the object and the field (passthrough: object Deployment "demo/web": spec.template.spec.containers[0] "app": …).A core Secret is refused under a policy that forbids explicit secrets (
oam.ExplicitSecretPolicy, see thepkg/oamREADME), as thesecrettrait is (go-kure/launcher#794, item 2):passthrough: object Secret "demo/creds": the object is a Secret, and the environment policy forbids explicit secrets; …. The check does not read what the Secret holds, so one with no entry is refused too, and the message quotes nothing of the object. A policy that does not implement the interface, and no policy, allow it. Breaking only under a policy that answersfalse: apassthroughSecret built under it before.The object is authored as a map and the check reads Go types, so an object whose group, version and kind kure’s scheme registers is decoded as that kind for the check only. What is emitted stays the authored map, so a field the Go type does not declare is emitted with it, and a large integer is emitted as authored, which kure’s manifest writer writes with its own digits (
helmtemplate’s Rendered objects). The check cannot read such a field, so a workload or a claim that sets one is refused, as template delivery andmanifestsrefuse it (helmtemplate’s Undeclared fields): when the component is built and again byGenerate, under any policy and with none (passthrough component "web": object Deployment "demo/web": undeclared field spec.template.spec.fieldOfALaterVersion: …). A workload or a claim in which a value that serializes itself (a Go caller’sjson.RawMessage) repeats enough keys to fill the strict decode’s record is refused on the same ground. An object of any other kind is emitted as authored, the field with it, whatever the strict decode says of it: nothing it sets is dropped, so the full-record refusal ofmanifestshas no part here. Breaking (go-kure/launcher#794, item 7): a workload or claim with an undeclared field was emitted unchecked before and no longer builds.Generateruns the check again on the object it is about to emit, onceApplyPolicyhas supplied a policy, becauseObjectis an exported field and the map checked need not be the map emitted; a refusal there is the same*oam.ViolationError, naming the component.What cannot be read is refused, not passed: an object of a registered kind that does not decode as that kind (
replicas: three), a workload kind, a claim or a PersistentVolume in an API version the scheme does not register (batch/v1beta1,apps/v1beta2), a HorizontalPodAutoscaler in such a version whosemaxReplicasis not an integer, and an object that does not serialize. So is an object the decoder of its kind panics on, which crashed the build before: aCiliumNetworkPolicyorCiliumClusterwideNetworkPolicywhoseicmpsfield leaves itstypeout, for one (passthrough: object CiliumNetworkPolicy "demo/p": the object cannot be read, …: the decoder panicked on CiliumNetworkPolicy "demo/p": …, the parse error kure gives such an object since go-kure/kure#1009). A MetalLBBGPPeeratmetallb.io/v1beta2is a registered kind since go-kure/kure#1007, so one that does not decode as that kind (holdTime: [1]) is refused here too; it was emitted unread. Since go-kure/kure#1014 an object of a registered kind that statesKindorapiversionbeside the exact key is refused here as well, with the parser’s text for the cause (the key "Kind" equals "kind" only after case folding; …). Breaking: theKindcase was read as its Go type and checked, and so was theapiversioncase where that key states a version the kind is registered in (apiversion: v1besideapiVersion: v1); with another version there it was unreadable before, for another cause. Every build decodes the object, since the transform appliesNoopPolicywhen no policy is passed; only a config no policy was applied to, one a Go caller builds outside the transform, emits the object undecoded, as authored.Behaviour change: before go-kure/launcher#794 a
passthroughobject reached the output unchecked. A document that relied on that no longer builds when its object is a workload with a privileged container, a host namespace, a hostPath volume, an image outside the registry allowlist, an untagged or:latestimage, or a value over a maximum — and with no policy passed the first three are always refused, sinceNoopPolicydenies them. A consumer allows what it wants allowed through its policy, not per component:AllowPrivileged(),AllowHostNetwork(),AllowHostPID(),AllowHostIPC()andAllowHostPathVolumes()for the security flags, the registry inAllowedRegistries()(or no allowlist), the capability lists, and the maxima (MaxReplicas(),MaxCPU(),MaxMemory(),MaxStorageSize()). An untagged or:latestimage has to be pinned in the object, and a workload in an API version the build cannot read has to be authored in the one it can (batch/v1,apps/v1).Not checked: an object of a kind not named above; a custom resource, the pods its controller creates and the replica count it sets; what a
Secretholds (a core Secret is told by its group and kind alone, and refused only under a policy that forbids explicit secrets). A nil policy (a directApplyPolicy(nil), orGenerateon a config no policy was applied to) checks nothing, andApplyPolicy(nil)withdraws no policy applied before.- A kind whose scope the Kubernetes API governs (a built-in kind, a
crd / manifests —
inlinexorurl;manifestsaddsscopeOverrides(apiVersion/kind/scope), the author’s explicit statement of a kind’s scope. An override outranks kure’s own non-API-governed scope-table entry — a kind nothing else can scope, e.g. a cluster-scoped custom resource whose CRD is installed out of band. It does not outrank aCustomResourceDefinitionbundled in the same source: that CRD is the definition being applied, so it must agree with the override rather than be overridden by it — a disagreement is rejected (manifests.go’sstampManifestNamespaces, an error naming both the override’s and the CRD’s declared scope; the resolution itself isresolveObjectScope, which thehelmtemplatecomponent’sscopeOverridesgoes through too); an agreeing override still applies, and a kind with no same-source CRD falls back to the non-API-governed case above. It is also ignored for a kind the Kubernetes API itself governs (isAPIGovernedScope,manifests.go: aCustomResourceDefinitiondocument; any kind whose kure scope-table entry comes fromScopeSourceBuiltin— i.e. from the generated upstream types,PriorityClass,APIServiceand the two webhook configurations among them; and any cluster-scoped API built-in kure fixes the scope of without registering it, a set that is empty at the kure version ingo.mod. Such a kind is detected by askingmanifest.Scopeitself with no CRD context, not by copying kure’s list, so kure stays the single answer); a manifest cannot redefine those. A kind with no override and no other scope source still fails closed when it carries nometadata.namespace. Conversely, a cluster-scoped object (either component, any scope source) that authorsmetadata.namespaceis rejected rather than emitted as-is — the Kubernetes API forbids a namespace on a cluster-scoped object, so letting it through would only defer the failure to apply time. Both components decode a document of a registered kind into its Go type, with the decodehelmtemplategives a rendered chart, and treat a field that type does not declare — one a newer Kubernetes version added, say — as it does (helmtemplate’s Undeclared fields): a workload or a claim that sets one is refused, under any policy and with none (manifest source: parse manifests: Deployment "demo/web": undeclared field spec.template.spec.fieldOfALaterVersion: …), an inline source when the component is built and a url source when it is fetched; an object of any other registered kind is emitted as written, unstructured, the field kept, and aCustomResourceDefinitionkept so still passescrdand still gives its scope to the resources beside it. Breaking (go-kure/launcher#794, item 7), in the same two ways: a source with such a workload or claim no longer builds, and the output of other kinds gains the fields that had been dropped. The same known limit applies (a key inside a type that unmarshals itself, a CRD’sitemsschema for one, is still dropped). A document the decoder of its kind panics on is a build error naming the object, as it is there (manifest source: parse manifests: parse error in Kubernetes object: failed to decode object: the decoder panicked on CiliumNetworkPolicy "demo/p": …), and so is malformed JSON the decoder cannot read past, on which the build did not return before go-kure/kure#1012. A MetalLBBGPPeeratmetallb.io/v1beta2is a registered kind since go-kure/kure#1007 and is read as one, with what that changes (helmtemplate’s Rendered objects). An integer the object holds as one is written with its own digits, here as everywhere (9007199254740993as9007199254740993;helmtemplate’s Rendered objects). In an unstructured object an integer an int64 cannot hold is a float from the decode on, and is written as that float.Policy. Every object a
manifestsorcrdsource yields is checked against the environment policy with the check template delivery (helmtemplate) andpassthroughuse: each Pod, PodTemplate, ReplicationController, Deployment, StatefulSet, DaemonSet, ReplicaSet, Job and CronJob is checked as an authored workload is (host namespaces, hostPath volumes, the cpu/memory maxima, the registry allowlist on every init and regular container’s image and on an image volume’s reference, the privileged, HostProcess and capability gates,ValidateImageRefon both, no ephemeral containers), a PersistentVolumeClaim and a StatefulSet’s claim template are held to the storage maximum, and a replica count and a HorizontalPodAutoscaler’smaxReplicasto the replica maximum. A PersistentVolume is held to what thepersistentvolumekind holds its own to: ahostPathorlocalsource needsAllowHostPathVolumes(), andspec.capacity.storageis held to the storage maximum (breaking for a source that holds one; see persistentvolume). An image volume from a registry outside the list is refused since go-kure/launcher#790 (breaking for a source that holds one; see Image volumes under pod). The error names the object and the field (manifest source: object Deployment "demo/web": spec.template.spec.containers[0] "app": …). Acrdsource holds only CustomResourceDefinitions, none of which the check reads, socrdbuilds as before.A core Secret a
manifestssource yields,inlineor fetched, is refused under a policy that forbids explicit secrets (oam.ExplicitSecretPolicy), as thesecrettrait and apassthroughSecret are (go-kure/launcher#794, item 10):manifest source: object Secret "demo/creds": the object is a Secret, and the environment policy forbids explicit secrets; …. The check does not read what the Secret holds, and the message quotes nothing of it. Breaking only under a policy that answersfalse. Not covered: a Secret a chart renders under template delivery (helmtemplate,helmwithdelivery: template), whose content comes from the chart and its values; the document’s own sensitive values are refused there where they are set (secretValues).When the check runs depends on the source. An
inlinesource is checked byApplyPolicy, the transform’s policy step, and the refusal is that step’s*oam.ViolationError. Aurlsource is checked inGenerate, on the objects it is about to emit:ApplyPolicychecks its host against the allowed registries and does not fetch, so a caller that runs the transform and never generates does not see an object refusal for aurlsource. A refusal inGenerateis the same*oam.ViolationError, naming the component; a fetch or parse failure there keeps its own error and is not a policy violation.Generatealso checks aninlinesource again, on what it emits.What cannot be read is refused, not passed: a workload, claim or PersistentVolume in an API version kure’s scheme does not register (
batch/v1beta1,apps/v1beta2), inside a list as outside one. A list is replaced by its items, each decoded and checked as a document of its own is, for a field its type does not declare too (manifest source: parse manifests: item 0 of List: Deployment "demo/web": undeclared field …): av1List, a typed list (DeploymentList) and, since go-kure/kure#1014, a list of a kind the scheme does not register (a kind ending inListthat statesitems), whose items of a registered kind are their Go types and are checked for real, a list among them opened in turn. The list handling is the onehelmtemplate’s Rendered objects describes, with the same errors undermanifest source: parse manifests: …: a label or an annotation on a list’s own metadata, aKind,apiversionorItemskey, anullitem, and an object of a kind that is no list with a top-levelitemsarray, which is refused with a policy or with none and is no policy violation. Breaking (go-kure/launcher#790): the table underhelmtemplatelists what built before and is refused now.Behaviour change: before go-kure/launcher#794 the objects of a
manifestssource reached the output unchecked. A document that relied on that no longer builds when its source holds a workload with a privileged container, a host namespace, a hostPath volume, an image outside the registry allowlist, an untagged or:latestimage, or a value over a maximum, or an object the check cannot read (above) — and with no policy passed the first three are always refused, sinceNoopPolicydenies them. A consumer allows what it wants allowed through its policy, not per component, exactly as forpassthrough. An untagged or:latestimage has to be pinned in the source, and a workload in an API version the build cannot read has to be authored in the one it can (batch/v1,apps/v1).Not checked: an object of a kind not named above; a custom resource, the pods its controller creates and the replica count it sets; what a
Secretholds (a core Secret is told by its group and kind alone, and refused only under a policy that forbids explicit secrets). A nil policy (a directApplyPolicy(nil), orGenerateon a config no policy was applied to) checks nothing, andApplyPolicy(nil)withdraws no policy applied before.A
urlthat does not parse is refused without the URL or the parser’s error, and a fetch error names the URL by scheme and host only (manifestsource.go’sdisplayURL; a URL with no host is not named): its userinfo, path or query may carry a credential, and an IPv6 zone in the host is dropped too. A refused redirect hop reads fixed text (redirect to a url that is not http(s) refused,redirect to a host not in allowed registries refused,too many redirects), since its target is the server’s. A failed request or body read is named by fixed text only (failureCause): a timeout, a failed host lookup, or the failing network operation and its system error, such asdial failed: connection refused; anything else readsrequest failedorresponse body could not be read. The underlying error’s own text is never shown, since it can quote the URL, a server-sent header or trailer, or a TLS certificate, but it stays the error’s cause:errors.Isanderrors.Asstill find it, so a timeout matchescontext.DeadlineExceeded.
StatefulSet-level and claim-template properties
The statefulset kind projects the whole appsv1.StatefulSetSpec field set it
owns, and each volumeClaimTemplates entry projects the whole
corev1.PersistentVolumeClaimSpec. Every field below is written only when
authored, so an unauthored StatefulSet emits no podManagementPolicy at all and
an empty updateStrategy: {} (a non-pointer struct, which omitempty does not
suppress), leaving the apiserver to default both. Until go-kure/launcher#361
kure’s CreateStatefulSet wrote podManagementPolicy: OrderedReady into the
object itself; the release-1 builder contract makes that constructor
identity-only, so the line is gone from the emitted manifest. The effective
policy is unchanged — OrderedReady is also the API default — but the manifest
text moved, which is why three goldens lost a podManagementPolicy: OrderedReady line. The claim entry’s five
pre-existing keys (name, mountPath, size, storageClass, accessModes)
now sit inside a closed key set, so a typo in an entry is reported instead of
being silently dropped. devicePath joined that set with go-kure/launcher#385.
Columns match the pod-level table above: additive means no document that built before builds differently now; behavior-changing means an existing document’s meaning or acceptance moved.
| Property | Type | Effect | Kind |
|---|---|---|---|
podManagementPolicy | enum | OrderedReady (also the apiserver’s default for an unauthored StatefulSet, so authoring it is a no-op that only pins the value in the manifest) or Parallel. | additive |
updateStrategy{type, rollingUpdate{partition, maxUnavailable}} | object | type is RollingUpdate or OnDelete; rollingUpdate is rejected under OnDelete, mirroring ValidateStatefulSetSpec. type is required only for an otherwise empty updateStrategy: {}, whose whole meaning would come from apiserver defaulting; when rollingUpdate is authored, RollingUpdate is inferred, since it is both the API default and the only type that reads the field. partition is >= 0. maxUnavailable takes a positive integer or a 1–100% string; the schema leaf is the Types: [integer, string] union (go-kure/launcher#383), so a schema-validating consumer accepts both forms. | additive |
revisionHistoryLimit | int ≥ 0 | Retained controller revisions. | additive |
minReadySeconds | int ≥ 0 | Readiness settling time before a pod counts as available. | additive |
persistentVolumeClaimRetentionPolicy{whenDeleted, whenScaled} | object | Each is Retain or Delete. | additive |
ordinals{start} | object | start shifts the replica ordinal range and is required once ordinals is authored; >= 0. | additive |
selector (claim) | object | matchLabels/matchExpressions, with the operator arity rule — In/NotIn need at least one value, Exists/DoesNotExist none — and every values entry validated as a label value, the check ValidateLabelSelectorRequirement runs on a newly created claim template. An entirely empty selector is rejected: the apiserver would accept it as matching every volume, which is never what an author who wrote the key meant. Authoring a selector opts the claim out of dynamic provisioning: a claim with a non-empty selector is never provisioned from its StorageClass and stays Pending until a pre-provisioned PV matches (Kubernetes: persistent volumes — Selector). | additive |
resources{requests,limits} (claim) | object | storage is the only accepted resource name — ValidatePersistentVolumeClaimSpec reads requests[storage] and nothing else, so any other name would be silently ignored. Quantities must be positive. apply merges requests onto the claim-template literal createStatefulSet already built (statefulset.go, which writes requests.storage from size), so size survives when only limits is authored. requests.storage is the long spelling of size; authoring both is an error. | additive |
volumeMode (claim) | enum | Filesystem or Block. Filesystem pairs with mountPath; Block pairs with devicePath and renders a main-container volumeDevices entry instead of a volumeMount (go-kure/launcher#385, see “Raw block volumes”). Until then Block was rejected, because every claim was mounted as a filesystem and the apiserver would have accepted the mismatched claim/pod pair. | additive |
devicePath (claim) | string | Where a volumeMode: Block claim appears in the main container as a raw block device. Not a claim-spec field. Exactly one of mountPath and devicePath is required, and devicePath only with volumeMode: Block. | additive |
dataSourceRef{apiGroup,kind,name,namespace} (claim) | object | Mirrors upstream validateDataSourceRef: kind and name are required non-empty with no format rule (a Kind is a CamelCase identifier, not a DNS name), apiGroup must be a DNS-1123 subdomain when non-empty, an omitted or empty apiGroup pins kind to PersistentVolumeClaim (the core group holds no other populator), and namespace, when set, is a DNS-1123 label (ValidateNamespaceName), not a subdomain. | additive |
volumeAttributesClassName (claim) | string | DNS-1123 subdomain naming a VolumeAttributesClass. | additive |
storageClass (claim) | string | Behavior-changing. A non-empty value is now validated as a DNS-1123 subdomain (ValidateClassName, the check ValidatePersistentVolumeClaimSpec runs and the one the volumes[].pvc path already applied). An invalid class name previously built a claim and was refused later by the apiserver; it is now refused here. A present-but-non-string value is likewise rejected rather than read as absent and provisioned through the cluster default class. | behavior-changing |
storageClass (claim) — "" and the pvc capability | string | Pre-GA output change (go-kure/launcher#761). An authored storageClass: "" now requests no class: the claim template carries storageClassName: "", as a volumes[].pvc claim and the persistentvolumeclaim kind do. It used to read as absent, so the template carried no class and the cluster’s default applied. An unauthored entry (absent or null) now takes the ClusterProfile pvc capability’s storageClassName, as an authored pvc trait does: the handler implements ComponentCapabilityFiller, reads the binding through LoweringContext.Capability once the component has an entry, so pvc is in ConsumedCapabilities exactly then, and fills each unauthored entry. An authored class, "" included, wins; a non-string rendering value is refused. Either change alters an existing StatefulSet’s volumeClaimTemplates, which the apiserver refuses to update: the StatefulSet fails to apply until it is recreated (the force-replace trait recreates it on apply, restarting its pods). The claims it already made keep their class, unless the retention policy deletes them with it. | behavior-changing |
size (claim) | quantity | Behavior-changing. size is now the short spelling of resources.requests.storage rather than a key of its own, and it is no longer unconditionally required — either spelling satisfies the requirement, and authoring both is an error. It must also now be positive: 0 was accepted before and is rejected now, matching upstream’s ValidatePositiveQuantityValue on requests[storage]. (The volumes[].pvc.size surface on every kind applies the same positivity rule since go-kure/launcher#384.) Because “authored” is now read through parseStringField, an explicitly empty size: "" no longer counts as authoring the short spelling, so pairing it with resources.requests.storage is accepted rather than rejected as a double spelling. | behavior-changing |
| any unrecognized key on a claim entry | — | Behavior-changing. A volumeClaimTemplates entry is now a closed key set. The previous parser called no rejectUnknownKeys at all on the entry map, so a misspelled storageClassName was dropped and the claim was provisioned through the cluster default class with nothing said. Such a document built before and errors now, which is a break of the additive test in docs/oam/design-gvk.md under an unchanged launcher.gokure.dev/v1alpha1. It is taken deliberately and on precedent: 6aed090 (feat(format): enforce policy and support securityContext on init/sidecar containers) shipped the same tightening on init/sidecar container entries. The format commit scope is what signals it — cliff.toml groups those commits under a Document Format changelog heading. | behavior-changing |
| any optional field above, authored as null | — | Not a wrong type — absence. Every optional StatefulSet-level and claim-level field added here reads an explicit null as omission, so updateStrategy: with nothing after it builds exactly as omitting the key does. This matters beyond hand-written YAML: a lowering rule may emit a nil, pkg/oam’s emission validation accepts it (“a nil under an optional field constrains nothing”), and a parser that answered “must be an object, got <nil>” would let a component satisfy the published schema and then fail conversion. Where the null lands on a field that is required once its parent is authored (ordinals.start, a claim’s name), it surfaces as the requiredness error, never as a type error. This covers the optional lists as well as the scalars and objects (selector.matchExpressions, a match expression’s values), and it covers a typed nil — map[string]any(nil) or []any(nil) inside an any, which is what a lowering rule assembled in Go produces for an unset optional and is not == nil — because pkg/oam’s isNullValue classifies those as null too. A key the schema rejects outright is unaffected: authoring it as null still earns the explanatory refusal rather than silence. go-kure/launcher#394 extended the same treatment to the shared field helpers and to the parsers it converted, so it reaches well beyond these fields — but not the whole package. The lifecycle and probe handler keys, where presence itself is the rule being enforced, refuse a null; since go-kure/launcher#570 a container’s resource quantities under any name and the top-level keys once read with a bare lookup (listed with the null row of the deployment table above) read it as omission. | additive |
volumeClaimTemplates itself — wrong type | list | Behavior-changing. The previous parser answered “is it present” and “is it a list” with one type assertion, so volumeClaimTemplates: {name: data} or a stray scalar was indistinguishable from an absent key: the StatefulSet was built with no claim templates and nothing was reported. A present-but-non-list value is now rejected naming the type received. An explicit null is absence, not a wrong type — volumeClaimTemplates: with no value decodes to nil, the key is optional in the schema, and pkg/oam’s own requiredness check (isNullValue) already reads nil as absent, so a document written that way is valid launcher.gokure.dev/v1alpha1 today and keeps building. | behavior-changing |
name, mountPath (claim) — collisions | string | Behavior-changing. A claim template’s name must differ from every other claim template’s and every volumes entry’s, and its mountPath from every other main-container mountPath, claim templates and volumes together. Each parser used to check only its own entries, and the apiserver does not catch the overlap: StatefulSet validation skips the pod template’s volumes. The controller keys claim templates by name, so of two sharing one only one claim was created, and it replaced a pod volume named like a template with that template’s claim, so the authored volume was never mounted. A repeated mountPath built and then failed pod creation. Such documents now fail at build time. The configmap and external-secret traits refuse a volume named like a claim template for the same reason. | behavior-changing |
name, mountPath, size (claim) — wrong type | string | Behavior-changing. All three previously read through a bare type assertion, so a present-but-non-string value (YAML parses size: 10e9 as a float and mountPath: 7 as an int) collapsed to the empty string and was reported as the requiredness error — “missing required field ‘size’” for a field the author had written. Each now rejects with an explicit type error naming the field and the type received. Acceptance does not widen: every one of these documents failed before and fails now, but with a message that points at the real defect. | behavior-changing |
Deliberately not accepted — each is rejected with an error naming the reason rather than silently ignored:
spec.selector(StatefulSet-level) — the builder derives it from the component name, and a StatefulSet’s selector is immutable after creation.spec.template(StatefulSet-level, go-kure/launcher#790) — the pod template is projected from the component’s own container and pod-level properties, as ondeployment. The parser dropped an authored one before.volumeName(claim) — pre-binding a claim template to one named PersistentVolume would point every replica at the same volume.volumeMount(claim) — not a claim-spec field at all; the container mount is authored asmountPath(or, for avolumeMode: Blockclaim,devicePath) on the same entry.dataSource(claim) — whendataSourceRefcarries nonamespacethe apiserver mirrors it into the supersededdataSourcefield, so authoring both is redundant; when it does carry anamespacethe apiserver does not mirror, anddataSourcemust stay empty. Either way the field is authored throughdataSourceRefalone.
The three claim-level refusals sit inside a volumeClaimTemplates entry, so
their reason reaches a caller of the handler; in a document the
authored-property check refuses the key first, with its own text and no reason
(see the section below).
Upstream fields a hand-parsed kind does not read
Ten kinds read their properties key by key instead of decoding them into the
Kubernetes type: deployment, statefulset, daemonset, job, cronjob,
service, persistentvolumeclaim, configmap, secret and serviceaccount. The first
five build pods and are “the five pod-building kinds” below. For such
a kind an upstream field its parser does not know is no decode error; it is a
key the parser never looks at. The rule that closes that gap
(go-kure/launcher#790):
Every field of the upstream type that such a kind does not read, under its name or in another shape, is an entry of one of the kind’s refusal maps with its reason. No upstream field is dropped in silence.
webservice and worker lower to a deployment and refuse what it refuses;
the pvc trait reads a claim through the persistentvolumeclaim kind’s
parser and refuses what that kind refuses, and the secret trait reads its
Secret through the secret kind’s parser (ParseSecretProperties).
The two paths
An authored key reaches a kind on one of two paths, and both give the reason:
- A caller of the handler (
ToApplicationConfig, orTransformwithout the check below) gets the reason alone:scheduling: not read by this component — alpha upstream, behind the WorkloadWithJob feature gate. - A document goes through the authored-property check first
(
Transformer.ValidateAuthoredProperties, whichkurel buildcalls). The check refuses a key the kind does not declare before the parser runs, and appends the kind’s reason to its own text:component "batch" (type "job"): properties: unsupported field "scheduling" (allowed: …); scheduling: not read by this component — …. The kind gives the reason throughUnsupportedFieldHint(pkg/oam’s README, “A reason on a refused key”).
Below the top level
UnsupportedFieldHint answers for a kind’s top-level keys. A refusal that
sits inside a property the kind declares reaches a caller of the handler with
its reason. In a document, the check refuses the key at that position with its
own text and appends the same reason, which the kind gives through
NestedUnsupportedFieldHint (go-kure/launcher#790):
properties.affinity: unsupported field "nodeAffinity" (allowed: …); affinity.nodeAffinity: not read — ….
The positions are:
| Position | On | Keys |
|---|---|---|
a container’s resources: the main container’s, and an initContainers or sidecars entry’s | every workload kind | claims |
the pod’s podResources | every workload kind | claims (upstream validation forbids claims at pod level: “claims may not be set for Resources at pod-level”) |
an initContainers entry | every workload kind | probes, lifecycle (see Container fields) |
the affinity shorthand | webservice, worker | nodeAffinity, podAffinity, podAntiAffinity |
a volumeClaimTemplates entry | statefulset | volumeName, dataSource, volumeMount |
A key that is no field of the upstream type is not in a refusal map. The
check refuses it as it refuses any undeclared key, with nothing appended; a
caller that skips the check has it dropped, as pkg/oam’s README states for
every handler. The one exception is the six CronJobSpec keys job refuses by
name, which a cronjob document retyped to job leaves behind.
Refused, with the reason
| Key | On | Why |
|---|---|---|
selector | deployment, statefulset, daemonset (webservice, worker) | Builder-managed (app: <component>), equal to the template labels, immutable once created. |
selector, manualSelector | job, cronjob | The job controller generates the selector from a per-job label; a hand-written one adopts another job’s pods. |
scheduling | job, cronjob | Alpha upstream, behind the WorkloadWithJob feature gate. |
restartPolicy, activeDeadlineSeconds (the pod’s) | deployment, statefulset, daemonset (webservice, worker) | apps/v1 validation accepts only Always, and no deadline, on these pod templates. On job and cronjob both names are read: the pod’s restartPolicy, and the JobSpec’s activeDeadlineSeconds. |
ephemeralContainers, priority, overhead, serviceAccount | every workload kind | See Pod-level properties. |
evictionResponders | every workload kind | Alpha upstream, behind the EvictionRequestAPI feature gate. |
restartPolicyRules (the main container’s) | every workload kind | Upstream accepts a container’s restart rules only with the container’s own restartPolicy, which no kind reads on the main container. An initContainers entry reads both (see Container fields). |
externalIPs, clusterIPs | service | See the service kind. |
dataSource | persistentvolumeclaim, the pvc trait | Authored through dataSourceRef. |
secrets | serviceaccount | The list limits mountable Secrets only under an annotation upstream deprecates since Kubernetes 1.32; it is no way to find or create a token. |
resources.claims | a container’s resources, on every kind that reads them | An entry names one of the pod’s resourceClaims, and a container’s resources are read without them; upstream puts the field behind the DynamicResourceAllocation feature gate. See Below the top level. |
podResources.claims | every workload kind | Upstream validation forbids claims in a pod’s own resources (“claims may not be set for Resources at pod-level”); the pod’s claims are its resourceClaims. See Below the top level. |
Read in another shape
These upstream names are free on the kind and are refused too, with a reason that names the properties the field is authored as:
| Upstream field | On | Authored as |
|---|---|---|
the pod template | the five pod-building kinds | The component’s own container and pod-level properties. Its metadata is not authored either: the kind’s builder writes the app: <component> label and nothing else, the transform then adds the component label (<domain>/component, see The app label), and a trait may add more. |
jobTemplate | cronjob | The component’s own job-level, pod-level and container properties. |
containers | the five pod-building kinds | The main container is the component’s container properties; further ones are sidecars entries on the kinds that have them. |
the main container’s name | the five pod-building kinds | Not authored: the container is named after the component. |
livenessProbe, readinessProbe, startupProbe | the five pod-building kinds | probes.liveness, probes.readiness, probes.startup. |
volumeMounts, volumeDevices | the five pod-building kinds | A volumes entry’s mountPath, or its devicePath for a volumeMode: Block claim. |
resources (the claim’s) | persistentvolumeclaim, the pvc trait | size, the storage request. A claim’s limits are not read. |
Three pod fields are read under another name, because their upstream name is a
property of the kind already: securityContext as podSecurityContext and
resources as podResources (both names are the main container’s), and on
job and cronjob the pod’s activeDeadlineSeconds as
podActiveDeadlineSeconds (the name is the JobSpec’s).
One level down
Inside a container, go-kure/launcher#790 reads securityContext.windowsOptions
on every container, and an init container’s own restartPolicy and
restartPolicyRules (see Container fields). Three upstream
fields one level down stay unread, and are refused at their position as an
unknown key:
lifecycle.stopSignal: upstream puts it behind theContainerStopSignalsfeature gate, alpha and off by default through Kubernetes 1.37, and holds the signal to the pod’sos.name. A container’slifecyclereadspostStartandpreStop.ports[].hostPortandports[].hostIP: they bind a port on the node. No environment-policy switch covers that (AllowHostNetwork()gates the node’s network namespace, not a port bound from the pod’s own), and whether the port is free is the scheduler’s question, on cluster state. Aportsentry readscontainerPort,nameandprotocol.
Not authorable, and not refusable by name
One case has no key a refusal could sit on: a name taken on this kind by
another upstream field of the same name. On job and cronjob,
restartPolicy is the pod’s, so the main container’s own restartPolicy
cannot be authored and cannot be refused. On cronjob, suspend is the
CronJob’s, so the job template’s suspend cannot either (see “One JobSpec
field is deliberately not shared” above).
Outside the properties
The object’s envelope is not a property of the kind’s own schema: its
apiVersion and kind are the component’s type, its metadata is the
component’s name and the objectName, labels and annotations properties
every kind takes, and status is the cluster’s. spec is the level whose
fields the properties are, and has no key of its own.
An explicit null on a refused key
A null carries no content, and the kinds differ on what the parser makes of it on a key they refuse. This is each kind’s behavior from before go-kure/launcher#790, left as it was:
- Read as unauthored:
serviceandpersistentvolumeclaimon every key they refuse, anddeploymenton the pod-level and container-level keys (not onselectorandtemplate). - Refused, as any value is:
statefulset,daemonset,job,cronjob,serviceaccount, andwebserviceandworker; and every kind onresources.claims, onpodResources.claimsand on the keys refused under theaffinityshorthand.
In a document the difference does not show: the authored-property check refuses an undeclared key whatever its value, null included.
What holds this
TestHandParsedKinds_CoverEveryUpstreamFieldwalks the upstream type of each of the ten kinds, from the object down every level whose fields are the component’s properties, and fails on a field that is neither read, nor refused, nor placed in one of the classes above — so a field a laterk8s.io/apiadds cannot be dropped in silence. It stops at the properties: what a property holds inside is held to the upstream type only for a container’sresourcesand the pod’spodResources.TestRefusedKeys_BothPathsGiveTheReasonholds every entry of every refusal map to the two texts above;TestRefusedKeys_OneLevelDownholds the two refusals inside a declared property.TestHandParsedKinds_MatchExpressionsAtEverySelectorruns a match expression through each of the 47 label selectors these kinds read, to the generated object, and five defective ones to their refusal.TestHandParsedKinds_TemplateMetadataAndContainerNameandTestHandParsedKinds_ServicePortFieldsReachTheObjectpin the template’s metadata, the main container’s name and the six fields of a Service port.
Extending
Custom component types implement oam.ComponentHandler (CanHandle +
ToApplicationConfig) and are registered alongside the built-ins. Exported helpers:
ValidateImageRef (image policy), BuildPVC (PVC from a PVCConfig), and the
persistentvolumeclaim kind’s claim path, ParseClaimProperties,
ApplyClaimPolicy and GenerateClaim, which the pvc trait builds through. A custom
Generate() that builds standalone PVCs from a PVCConfig list should qualify their
names with the component name the way the role kinds do — see roleClaims
(unexported, role_members.go) — to avoid two components colliding on the same
pod-local volume name.
See pkg.go.dev
for the full type/field reference, the OAM model
for the handler interfaces, and examples/ for runnable applications.
The null contract
Every table above answers “what does an explicit null mean here?” one field at a
time. This is the rule they are all instances of:
A value that serializes to JSON null is absent, at every depth, on every path; a null is never a member of any
Itemstype, so a null array element is a type error; reservation is about the KEY being written.
It is written down because “null” has several readers — the emission validator’s
requiredness check and its optional-key strip, its array-element guard,
enforcePlatformReserved, this package’s parsers, and the Kubernetes API itself —
and each was aligned separately, so each round of review found the reader the last
round had not touched.
What it means for a handler parser in this package: read a null under an optional
property as omission, never as a present value of the wrong type. authoredValue —
the presence primitive parseStringField, parseObjectField, parseInt32Field,
parseInt64Field, parseObjectList and parseStringList all go through — does
exactly that, so calling any of them directly already complies. They classify a
typed nil as null too — map[string]any(nil) or []any(nil) inside an any,
which is what a lowering rule assembled in Go produces for an unset optional and is
not == nil.
This used to require a separate optional* wrapper family (optionalString,
optionalObject, optionalInt32, optionalInt64, optionalObjectList,
optionalStringList) layered in front of the plain parsers. go-kure/launcher#394
moved the handling into the shared helpers themselves, which made every wrapper an
exact no-op forward; they were removed and every caller now uses the parse* helper
directly.
TestNoOptionalFieldWrappers (no_optional_wrappers_internal_test.go) keeps them
removed: it fails on any function or method whose name starts with optional. It
parses every production .go file directly in this package’s directory — every
file, not only common.go, and never a _test.go file or a subdirectory — and
fails if it parsed none, so a wrong working directory cannot pass it vacuously. A
new optional field is read with the helper for its type instead: parseStringField
(or requiredStringField when the field is required), parseBoolField,
parseInt32Field, parseInt64Field, parseObjectField, parseObjectList or
parseStringList. Each already reads an explicit null as absence through
authoredValue, so a wrapper would only repeat that check.
Two limits worth knowing before relying on the rule:
A parser that answers presence with a bare
v, ok := props[key]does not comply on its own. It reads a present null as present. Emission validation deletes such keys before a rule-emitted component is converted, so on that path those parsers agree with the contract because of the strip, not independently of it. The authored path has no top-level strip:ValidateAuthoredPropertieschecks shape and leaves a top-level null in place, so there the null reaches the parser. What it does next depends on how the parser consumes the lookup: one that type-checks the value refuses it as a wrong type, while one that discards a failed assertion treats it as absent without saying so.go-kure/launcher#423,go-kure/launcher#394andgo-kure/launcher#570converted the parsers they covered; the last of them are listed with thenullrow of thedeploymenttable above.An empty object is not a null and is not absent.
{}and an absent key mean different things to Kubernetes wherever aLabelSelectoris involved, so the contract must never be read as licence to collapse them. What each one means is per field, not a property of selectors in general — the two selectors in a singlecorev1.PodAffinityTermdisagree, in upstream’s own words:field null {}labelSelector“matches with no Pods” matches every pod in scope namespaceSelector“this pod’s namespace” “matches all namespaces” So “empty matches everything, nil matches nothing” holds for
labelSelectorand is wrong fornamespaceSelector, where nil is the narrowest answer rather than the emptiest one.
Both limits met on one key, and it was the second row of that table.
namespaceSelector is parsed twice in this repository: here, for pod affinity,
through parseSchedulingSelector (parseObjectField), where any null is
omission — which upstream reads as “this pod’s namespace”; and in the
networkpolicy trait, through parseNPPeer. A typed nil used to satisfy the
networkpolicy trait’s own type assertion and yield an empty selector — all
namespaces, the widest possible answer — while an untyped nil already read as
omission there, so one input landed on opposite ends of the range depending on
which Go nil shape produced it. Both parsers now route their presence check
through oam.IsNullValue’s reflect-based classification, which treats a typed
nil the same as an untyped one, so the two halves converge: a null
namespaceSelector, however it was constructed, is omission on both paths.
Fixed as go-kure/launcher#430; networkpolicy_internal_test.go’s
TestParseNPPeer_NamespaceSelectorPresenceCases pins the typed-nil case
directly.
The same typed-nil shape reached list elements. parseObjectList read a
null key as absence, but a typed-nil element ([]any{map[string]any(nil)})
passed its per-element assertion as an empty object, where an untyped null
element is refused (tolerations[0]: must be an object, got <nil>). Most
callers then failed on a missing required field anyway. tolerations did not:
an empty entry became an Exists toleration with no key, which tolerates
every taint. The element is now normalised to a null first, so every caller
refuses both shapes identically (go-kure/launcher#465). The list readers that
do not go through parseObjectList (envFrom, httpHeaders, volumes,
volumeMounts, sidecar ports, the Job successPolicy/podFailurePolicy rules and
onPodConditions, volumeClaimTemplates, valuesFrom, scopeOverrides) pass each
element through nullElem first, for the same untyped refusal and message.
Conventions
Handlers use k8s.io/api constants for well-known Kubernetes enum values (access
modes, restart policies, etc.) rather than string literals — never re-define values
that already exist upstream.
Every generated object owns its label maps. Generate hands back objects the caller
owns and routinely edits — stamping ownership, environment or version labels onto the
workloads it emits — so no two fields and no two objects ever share one
map[string]string. Concretely: the object’s metadata.labels, the pod template’s
metadata.labels, the Service selector and the labelSelector of every topology
spread constraint and pod anti-affinity term are separate maps with equal contents.
Inside this package appLabels and selectorFrom (both unexported, common.go) are
what enforce that: call appLabels once per assignment rather than hoisting its result
into a variable used twice, and let selectorFrom copy what it is handed rather than
store it. A custom handler outside this package cannot call either and does not need to
— the rule is to build a fresh map[string]string at each assignment site, and to
maps.Clone any map before storing it in a selector.
This is a correctness rule, not tidiness. A selector that aliases the pod template’s
labels follows the caller’s edits, and a topology spread constraint’s labelSelector
defines the pod set over which skew is computed — so a version label leaking into it
makes a rollout spread each version separately instead of spreading the workload.
The same rule runs in the other direction: a generated object never shares a pointer
or a map with the config that produced it. A handler config is reusable — the same one
can be rendered more than once — so every value a *SpecConfig.apply projects onto a
generated object is deep-copied on the way out: the claim template’s selector,
resources.limits, dataSourceRef, volumeMode and volumeAttributesClassName, and at
the workload level updateStrategy (whose struct holds a pointer, so *c.UpdateStrategy
alone would not be enough), revisionHistoryLimit, persistentVolumeClaimRetentionPolicy
and ordinals. The deployment kind projects the same two shapes and follows the same rule:
strategy (holding a *RollingUpdateDeployment), revisionHistoryLimit and
progressDeadlineSeconds. applyJobSpec follows the same rule for the job and cronjob
kinds: its ten scalar pointers go through the generic copyPtr, and successPolicy and
podFailurePolicy — whose structs each own a slice of rules — through DeepCopy.
The pod template every workload kind shares follows it too: buildPodSpec starts from a
DeepCopy of the authored pod-level fields (nodeSelector, podSecurityContext,
terminationGracePeriodSeconds, imagePullSecrets, …) and deep-copies each volume,
toleration, topology spread constraint, the affinity, and the main, init and sidecar
containers as it adds them — an append alone copies the element structs but shares the
pointers, maps and slices they carry. The postgresql Cluster follows it for
spec.inheritedMetadata: its labels and annotations are clones of the config’s
InheritedLabels/InheritedAnnotations, not the maps themselves
(go-kure/launcher#396). Without that, editing the first rendered object — the same in-place
customization the label rule above assumes — writes back into the config and reappears in
every later render, with the symptom surfacing on a different object than the one that was
edited.
The app label
Every app label and app selector this package generates to identify a component is
valued at oam.ComponentLabelValue(<component>), never the raw component name —
appLabels and deploymentComponentLabels call it, and nothing else writes the value.
Authored values are emitted as written: an authored selector on a service component
replaces the generated one and is not projected, and a type that emits authored objects
(passthrough, for one) adds no app label. The one authored value that is checked is
an app label on the pod template of a replicaset or replicationcontroller component, which gets the generated
label beside its authored ones: the component’s own label value is kept, another is
refused. A component name
is a DNS-1123 subdomain (up to 253 characters), a label value at most 63: the function
returns a name of 63 characters or fewer unchanged, so output for those names is
byte-identical, and projects a longer one onto a readable prefix of at most 52 characters
(its first 52, with trailing - and . trimmed) plus - and
a 10-hex-character sha256 digest (go-kure/launcher#572). The workload kinds and service never reach
the projection, since their container name or Service name already refuses a name over 63
characters; the helm values ConfigMap and values Secret do (a values
ConfigMap that previously omitted app past 63 characters now carries the projected value). Object names are not projected. A custom handler
that labels its objects by component uses the same function, so its selectors and the
built-in traits’ selectors (a PodDisruptionBudget, a NetworkPolicy podSelector) agree.
The component label (<domain>/component) is not this package’s to write: the transform
adds it to every object a component’s config generates, and to its pod templates, with the
authored component’s value, where the key is absent (go-kure/launcher#788). Where the key is
there with another value, the transform’s output is refused when it is generated
(go-kure/launcher#790): that holds for what a component type of this package emits from
authored objects or a rendered chart (passthrough, manifests, helmtemplate, helm
under template delivery) as for any other, so the key is one nothing else writes. A lowered part
named differently from its component (the postgresql pooler, a database, an object store)
carries the component’s value, not its own name. A source a lowering rule generates and the
application bundle holds (the repository a helm component generates) carries none. A
HelmRelease also gets a post-renderer that sets the label on the chart’s pod templates.
See “Component label and ownership” in the
OAM model.
The object name (objectName)
Every kind component takes objectName, which names its one object in place of the component
name (go-kure/launcher#787): every component type with a handler
(builtinComponentHandlers) but four, as TestObjectName_EveryComponentTypeChooses
(pkg/cmd/kurel) holds; a type that is lowered to others has none. helmtemplate,
manifests, crd and passthrough generate no
single object named after the component and refuse it. The rules for the name, the Naming
hook’s role object and the references that follow it are under “objectName: the object of
a kind component” in the
OAM model.
The property is not in any handler’s PropertySchema: the engine adds it to the schema of
every type whose handler declares its object (ComponentObject, in object_name.go or, for
a kind added since, in the kind’s own file),
reads it, and hands ToApplicationConfig the resolved name as Component.ObjectName(). A
handler’s config carries it as ObjectName, empty when it is the component’s name, and
Generate names the object with it. Everything else the handler writes keeps the component
name: the app label and selectors, the pod template’s labels, the main container’s name.
A handler’s own check of the name (a Service’s DNS-1035 label, a CloudNativePG Cluster’s
length, a CronJob’s 52 characters, a Job’s 63 and the room an Indexed job’s last index
needs) runs on the object name, since that is what the object carries.
What a config tells a trait or the transform about its object follows the object name: a
service component’s BackendServiceName, a serviceaccount component’s
ServiceAccountName, and the pod selector of a cnpg-cluster or cnpg-pooler endpoint. Two
consequences are the kind’s own: a statefulset’s pods and claims take the StatefulSet’s
name, so they follow objectName; a helmrelease’s spec.releaseName default stays the
component name, so the release does not.
The object’s labels and annotations (labels, annotations)
Every kind component, the same types that take objectName, takes two optional properties,
labels and annotations, each a map of strings (go-kure/launcher#790). They go on the
metadata of the component’s one object and nowhere else. On a workload kind the pod template
keeps the labels of the kind, its app label and what template.metadata authors, and no
selector reads an authored label. helmtemplate, manifests, crd and passthrough
generate no single object named after the component and refuse both. A top-level spec
field named labels therefore cannot keep its name: a thanosruler’s Prometheus external
labels are its externalLabels property (see thanosruler); a prometheus’s are its
spec’s own externalLabels field, under its upstream name.
Refused, naming the component, the property and the key:
- a value that is no map, and an entry that is no string (quote a number or a boolean); a null value or entry is an absent one;
- a key or value the API server refuses: a label key or value that is no valid label, an annotation key that is no valid annotation key, annotations whose keys and values hold more than 262144 bytes together;
- the
applabel with another value than the component’s (its name, or the hashed form of a name over 63 characters), which is what the kinds that setappselect by; - the component label key with another value than the component’s (see “Component label and ownership” above);
- a key the kind already sets on its object with another value;
- a key the consumer reserved for itself (
ReservedMetadataKeys), as on every generated object.
A label that names another object is the author’s literal: it does not follow that object’s
objectName or the Naming hook’s answer for it. An object that belongs to another through
a label (an EndpointSlice’s kubernetes.io/service-name) is written with the name the other
object takes.
An annotation a delivery engine acts on (a Flux annotation such as
kustomize.toolkit.fluxcd.io/force, for one) is written as authored too: launcher does not
refuse it and does not carry out what the engine does with it. Transformer.WarnForcedVolumes
warns about a PersistentVolume or PersistentVolumeClaim that has the force key enabled, and
changes nothing. A consumer that keeps such keys to itself reserves them with
TransformContext.ReservedMetadataKeys.
As objectName, the two are in no handler’s PropertySchema: the engine adds them to the
schema of every type whose handler declares its object, reads and checks them, removes them
from the properties and hands ToApplicationConfig the result as
Component.ObjectMetadata(). A handler’s config carries it as Metadata and Generate puts
it on the object through kindObject, the one helper every kind returns its object through
(the Flux sources through emitFluxSource, which does the same before it writes a long
spec.timeout). TestObjectMetadata_EveryKindComponentTakesIt holds every registered kind
to the two properties, so a kind added later cannot ship without them.
A label selector’s match expressions
A kind component whose spec holds a Kubernetes label selector (metav1.LabelSelector) decodes
it strictly, and the strict decode refuses nothing of a match expression: the Go type writes
key and operator whether or not they were authored, and reads any string as an operator.
Three defects of an expression are therefore refused where the component is read, with or
without an environment policy, each named by its path (go-kure/launcher#790):
- An expression without its
keyor itsoperator(selector.matchExpressions[0].operator: required (the operator of the expression: In, NotIn, Exists or DoesNotExist)). The object would carrykey: ""oroperator: ""and not show the omission. - An
operatorthat is none of the four (selector.matchExpressions[0].operator: "Equals" is not a label selector operator (In, NotIn, Exists or DoesNotExist)). - An operator and
valuesthat do not go together:InorNotInwithout a value (selector.matchExpressions[0].values: required with the operator In (at least one value)),ExistsorDoesNotExistwith one (…values: not allowed with the operator Exists (it takes no value)).
The API server refuses each of the three on every selector listed below, through one function,
ValidateLabelSelectorRequirement of k8s.io/apimachinery, and under every option it passes
that function. TestValidateLabelSelector_MatchesAPIMachinery holds launcher’s check to the
linked function on those three.
That function judges more, and these kinds do not. It also refuses an expression key that
is no label name, an expression value that is no label value (on the objects that still check
it), and a matchLabels key or value that is no label name or value. Those are value rules,
left to the API server as for every typed kind: a selector with key: "not a label" builds
here and is refused at apply. An authored empty key (key: "") is one of them: it differs
from an unauthored key only in that the unauthored one is refused here, and the API server
refuses both.
Where it holds:
| Kind | Selectors |
|---|---|
networkpolicy | podSelector; the podSelector and namespaceSelector of every ingress[].from[] and egress[].to[] peer |
poddisruptionbudget | selector |
clusterrole | every entry of aggregationRule.clusterRoleSelectors |
validatingwebhookconfiguration, mutatingwebhookconfiguration | the namespaceSelector and objectSelector of every webhook |
pod, and under template.spec podtemplate, replicaset and replicationcontroller | the labelSelector and namespaceSelector of every podAffinity and podAntiAffinity term, required or preferred; a topology spread constraint’s labelSelector; a projected clusterTrustBundle source’s labelSelector; the selector of a generic ephemeral volume’s claim template |
A replicaset’s own selector is refused for the same defects by the check that compares it
with the app label, with apimachinery’s reason and not the texts above (see its entry);
TestReplicaSetSelector_RefusedAsAPIMachineryReadsIt holds that to each of them. The Cilium
kinds hold the key and operator of Cilium’s own selector type through the same required
list, with the same wording; their entries say what else Cilium requires.
Presence only, where a CRD defines the object. On the kinds below an expression without its
key or its operator is refused, by the same required list and in the same words. The CRD
requires both fields, and the Go type writes each whether or not it was authored, so the object
would not show the omission: the document as authored is one the API server refuses, and the
kind refuses the omission by presence, as it does every required field its type writes
unauthored. An authored empty string is a value, left to the API server. Nothing else of the
expression is refused: an operator that is none of the four and a mismatched values are left
to the API server and to the operator’s own admission, since a CRD’s schema requires the two
fields and says nothing of the pair.
| Kind | Selectors | Ground |
|---|---|---|
servicemonitor, podmonitor | selector | the linked type, by the generator’s rule (below) |
prometheus-probe | targets.ingress.selector | the same |
alertmanager | the selectors a pod holds, at the top of the spec; the selector of the claim template of storage.volumeClaimTemplate and of storage.ephemeral; alertmanagerConfigSelector; alertmanagerConfigNamespaceSelector | the same |
thanosruler | the selectors a pod holds, at the top of the spec; the selector of the claim template of storage.volumeClaimTemplate and of storage.ephemeral; ruleSelector; ruleNamespaceSelector | the same |
prometheus | the selectors a pod holds, at the top of the spec; the selector of the claim template of storage.volumeClaimTemplate and of storage.ephemeral; the selector and the namespace selector of the ServiceMonitors, PodMonitors, Probes, ScrapeConfigs and rules it reads (serviceMonitorSelector, serviceMonitorNamespaceSelector, podMonitorSelector, podMonitorNamespaceSelector, probeSelector, probeNamespaceSelector, scrapeConfigSelector, scrapeConfigNamespaceSelector, ruleSelector, ruleNamespaceSelector) | the same |
cnpg-cluster | the labelSelector and namespaceSelector of every affinity.additionalPodAffinity and affinity.additionalPodAntiAffinity term, required or preferred; topologySpreadConstraints[].labelSelector; a projectedVolumeTemplate clusterTrustBundle source’s labelSelector; the selector of ephemeralVolumeSource.volumeClaimTemplate.spec and of the pvcTemplate of storage, walStorage and each tablespace; podSelectorRefs[].selector | the Cluster CRD of the linked module |
cnpg-pooler | under template.spec, the selectors a pod holds | the Pooler CRD of the linked module |
issuer, clusterissuer | the labelSelector and namespaceSelector of every pod affinity and anti-affinity term of an HTTP01 solver’s podTemplate.spec.affinity, under ingress and under gatewayHTTPRoute | the Issuer and ClusterIssuer CRDs of the linked module |
gateway, listenerset | a listener’s allowedRoutes.namespaces.selector; a Gateway’s allowedListeners.namespaces.selector | the CRDs of both Gateway API channels |
cilium-nodeconfig | nodeSelector | the CiliumNodeConfig CRD of the linked module |
secretstore, clustersecretstore | conditions[].namespaceSelector | the linked type, by the generator’s rule (below) |
clusterexternalsecret | namespaceSelector; every entry of namespaceSelectors | the same |
imageupdateautomation | policySelector | the same |
resourcesetinputprovider | every entry of selectors | the ResourceSetInputProvider CRD of the linked module, and the linked type, by the generator’s rule |
replicationsource, replicationdestination | the labelSelector and namespaceSelector of every pod affinity and anti-affinity term of a mover’s moverAffinity, required or preferred: rclone, restic, rsyncTLS, and on a source syncthing | the ReplicationSource and ReplicationDestination CRDs of the linked module |
The Prometheus operator’s module, the External Secrets Operator’s and Flux’s image automation
module ship no CRD to read, so the ground of their kinds is the source of the linked
metav1.LabelSelectorRequirement; the Flux Operator’s module ships its CRDs, and its kind is
held to both, to its CRD and, as the other Flux kinds are, to that source: the schema generators
require a field that carries no optional marker and whose json tag keeps it when empty, and
key and operator are such fields
where values is not. TestMonitoringKinds_RequiredMatchMarkers,
TestExternalSecretsKinds_RequiredMatchSource and TestFluxKinds_RequiredMatchMarkers derive
the two from that source by that rule, beside the fields the operator’s own types mark
required, and hold each kind’s list to them; on the monitoring kinds another field of an
embedded Kubernetes type that the rule requires fails there unless it is named with the reason
it is not refused, except on alertmanager, thanosruler and prometheus, which embed the pod spec’s types
and are held to their match expressions only, as the pod kind is. CloudNativePG’s admission
webhook runs apimachinery’s whole selector validation on podSelectorRefs[].selector; that
fuller check is the operator’s, not launcher’s.
Not held: the metric selectors of a horizontalpodautoscaler
(metrics[].object.metric.selector, metrics[].pods.metric.selector,
metrics[].external.metric.selector). The API server’s validation of an autoscaler does not
read them, and a kind does not refuse what the API admits.
A config that is exported (NetworkPolicyConfig, PodConfig, PodTemplateConfig,
ReplicaSetConfig, ReplicationControllerConfig) repeats at Generate the two checks its
typed value can show: the operator, an empty one included, and the pair. A missing key shows
only in what was authored, so it is refused where the component is read.
TestLabelSelectorKinds_CoverEverySelector walks the type of every strictly decoded kind and
fails on a selector path that is neither held nor listed as left out with its reason, so a
selector added by a dependency bump or a new kind cannot go unread. For a presence-only kind it
also reads the CRDs named above, fails where one does not require key and operator at a
held path, and asks the API server’s own validation of a custom resource at each: the object
the kind emits for a whole expression is accepted, and the same object with the key or the
operator taken out is refused for that field alone. With the field as the empty string the
type writes, the object is accepted again, which is why the omission is refused where the
component is read.
The walk covers the kinds that decode a type. The hand-parsed kinds (deployment, daemonset,
job, cronjob, statefulset, persistentvolumeclaim) hold their selectors through their own
parser (parseLabelSelectorOpts), which refuses the same and more, so their absence from the
test’s list is not a gap.
Required fields a kind writes unauthored and does not refuse
A field the API requires can still reach the object unauthored. Where the Go type writes the
field whatever was authored ("", 0, {}, null), the object carries a value the author
did not write and does not show the omission. A kind refuses the omission of such a field only
where its required list or its own validation names it (helmrelease checks a valuesFrom
entry’s kind and name by hand); every other one is written and left to the API server and to
the operator. The section above and each kind’s entry list the ones that are refused. This
section states the ones that are not, as they are at this pin.
Families. The test names each field by the type that holds it:
| Family | Owner type | Typical fields |
|---|---|---|
AFF | the core/v1 affinity terms: NodeSelectorRequirement, PodAffinityTerm, PreferredSchedulingTerm, WeightedPodAffinityTerm | a node selector requirement’s key and operator; a pod affinity term’s topologyKey; a weighted or preferred term’s weight, podAffinityTerm and preference |
SEC | SeccompProfile, AppArmorProfile, Sysctl | a profile’s type; a sysctl’s name and value |
POD | any other core/v1 type | the key of a secret or config map key reference; a container’s name; an imageCatalogRef’s kind and name |
KIND | a type of the kind’s own API, or of metav1 other than a label selector’s match expression | an alertmanager’s ephemeral claim template’s ownerReferences[] fields; an HTTPRoute’s parentRefs[].name and header match name/value; a CloudNativePG secret reference’s name and key; a Database’s schemas[].name |
A fifth family of the test, SEL (the key and operator of a metav1 label selector’s
match expression), has no member: those are refused on every kind
(A label selector’s match expressions).
Per kind.
| Kind | Not refused | AFF | SEC | POD | KIND |
|---|---|---|---|---|---|
issuer, clusterissuer | 42 each | 36 | 6 | ||
replicationsource | 113 | 72 | 16 | 25 | |
replicationdestination | 85 | 54 | 12 | 19 | |
cnpg-cluster | 157 | 18 | 7 | 52 | 80 |
cnpg-pooler | 178 | 18 | 8 | 147 | 5 |
cnpg-database | 11 | 11 | |||
cnpg-objectstore | 29 | 9 | 20 | ||
httproute | 44 | 44 | |||
grpcroute | 34 | 34 | |||
servicemonitor, podmonitor, prometheus-probe | 19 each | 19 | |||
alertmanager | 241 | 18 | 8 | 207 | 8 |
helmrelease | 17 | 17 | |||
fluxcd-kustomization | 14 | 14 | |||
bucket, ocirepository | 7 each | 7 | |||
gitrepository | 6 | 6 | |||
helmchart, helmrepository | 3 each | 3 |
None is left on certificate, the eleven Cilium kinds, gatewayclass, gateway,
listenerset, referencegrant, backendtlspolicy, tcproute, udproute, tlsroute,
metallb-ipaddresspool,
metallb-l2advertisement, metallb-bgpadvertisement, metallb-bgppeer, cnpg-imagecatalog,
cnpg-clusterimagecatalog, cnpg-backup, cnpg-scheduledbackup, cnpg-databaserole,
cnpg-publication, cnpg-subscription, prometheusrule,
artifactgenerator, fluxcd-alert, fluxcd-provider, fluxcd-receiver, imagepolicy,
imagerepository, imageupdateautomation, resourcesetinputprovider, secretstore,
clustersecretstore, externalsecret and clusterexternalsecret. metallb-bfdprofile and
metallb-community are not measured: their CRDs require no field.
TestRequiredWrittenKinds_CoverEveryCRDKind holds the measured kinds to the kind inventory:
every kind row whose API group client-go’s scheme does not register, so a CRD serves it, is
measured or named as not measured with its reason, so a new kind component of such an API
fails there until it is one or the other. Most members are written
""; the rest are 0 (mostly a preferred term’s weight or a port), {}, [], null or
an object of such values.
What the API server makes of them is a value rule of each field, not read here. One case is
shown: imageCatalogRef of cnpg-cluster and pgbouncer.imageCatalogRef of cnpg-pooler.
Its kind written "" is refused by the CRD’s own rule on the reference. Its name written
"", and on cnpg-cluster its major written 0, are accepted by the API server and reach
the operator.
Excluded as valid values. A member whose written value the API takes as authored is not
listed: cnpg-cluster’s instances (1, the CRD’s default) and
postgresql.syncReplicaElectionConstraint.enabled (false, which the CRD requires and gives
no default; the Go type’s zero value is a valid one); of the Flux kinds, interval of
bucket, fluxcd-kustomization, gitrepository, helmchart, helmrelease and
ocirepository ("1h0m0s", the kind’s own default), a verification’s provider on
helmchart, helmrelease (chart.spec.verify.provider) and ocirepository ("cosign", the
API’s default, which the kind writes), and fluxcd-kustomization’s prune (false, a valid
zero value). The test names the twelve and checks that their omission builds and writes
exactly that value.
Method. For each field the kind’s API requires and its Go type writes unauthored,
TestKindComponents_RequiredWrittenNotRefused builds the kind with the field authored, with
the required fields around it filled, then builds it again with the field taken out. The field
is a member when the second build succeeds and the object still holds the field. A field under
a path the kind refuses whatever is authored there is skipped, and the test names those paths
with their reason: a cilium-networkpolicy’s nodeSelector, a cnpg-pooler’s
template.spec.ephemeralContainers, and the generatorRef of an externalsecret’s or a
clusterexternalsecret’s data[].sourceRef. A refused omission counts only where the error
names the field or one under it, by its full path; the two Cilium policy kinds refuse a label’s
key and an ICMP field’s type in their decode instead, with an error naming no field, and the
test names those with their reason. Any other field that cannot be measured fails the test. The
required lists are those of the CRDs of the linked modules. The Prometheus operator, Flux and
External Secrets kinds are read through the schema markers of their linked sources, by the same
reader as their own required-list tests; resourcesetinputprovider is read from the CRD the
flux-operator module ships. The members are pinned in
testdata/required-written-not-refused.txt, one line per field (kind, path, family, written
value). The test fails on any difference, a new member or one that is gone, so a dependency
bump or a kind change that moves the set fails CI; UPDATE_REQUIRED_WRITTEN_PIN=1 rewrites
the file, and is refused where CI is set.
go-kure/launcher#883 (deferred) tracks a mechanism for this class.
Every spec field is this package’s to write
Since go-kure/launcher#361 this package builds against kure’s release-1 builder
contract (go-kure/kure ≥ v0.2.0-beta.11). Under it a Create<Kind>
constructor returns an object carrying TypeMeta plus metadata.name and
metadata.namespace and nothing else — no labels, no annotations, no
selector, no injected defaults. Everything else is the handler’s own literal or
an explicit field assignment. Two consequences a reader needs:
spec.selector is written explicitly, and must agree with the pod template.
Deployment, StatefulSet and DaemonSet all require spec.selector and get no
server-side default for it, so each handler assigns
&metav1.LabelSelector{MatchLabels: …} from the same helper that produced
spec.template.metadata.labels — deploymentComponentLabels for deployment
and for worker, which lowers to a deployment component (its affinity selector
uses appLabels), and appLabels for webservice, statefulset and daemonset (both
return a fresh {"app": <label value>} map, per the ownership rule above, valued
as The app label describes). This is
the one field the compiler cannot check: a selector that disagrees with the
template labels compiles and is refused by the apiserver at apply time. job and
cronjob are the deliberate exception — the Job controller fills spec.selector
and its matching pod labels itself.
Two injected defaults are gone from the emitted manifests. The constructors
used to write imagePullPolicy: IfNotPresent onto every container and
podManagementPolicy: OrderedReady onto every StatefulSet; neither is emitted
now. OrderedReady is the apiserver’s own default for that field, so that one
is unchanged in effect. imagePullPolicy is written only when a component
authors it (see Container fields); what follows is about
the omitted case. Kubernetes defaults an omitted
value from the image reference and keys that decision on the latest tag, so
what matters here is that no accepted image can carry one: ValidateImageRef
(common.go) refuses an untagged reference and an explicit :latest tag on
every main, init and sidecar image — including a digest reference that still
carries the tag (repo:latest@sha256:...), which it rejects for exactly this
reason. A digest reference with no tag is the case this argument does not
reach, and no golden covers that form; it is pinned by digest either way, and
this package does not assert what a cluster defaults for it. The argument is
about the parsed path, which is where ValidateImageRef runs
(ToApplicationConfig): a handler config a library consumer builds directly
carries whatever image it was given, so :latest with no pull policy remains
reachable that way. That is also the only other path with a delta here — raw
manifests, passthrough, Helm-rendered objects and Flux patches never went
through the constructors, so nothing was ever injected into them to lose.
A third delta from the same contract change is metadata rather than a default, but
belongs in the same inventory: under valuesMode: configMap the generated values ConfigMap used to take an app label and an
app annotation from the constructor, both valued at the ConfigMap’s own name
(<component>-values). It now carries the label only, valued at the component’s
label value (<component>, projected when the name exceeds 63 characters — see
The app label), matching every other object this package emits. A consumer
selecting that ConfigMap by app=<component>-values must be repointed.
The obj.Annotations = nil
assignments scattered through the handlers, which existed to strip the app:
annotation the constructors used to stamp, are now no-ops — kept so the field
stays at a known value regardless of what a future constructor does.
postgresql writes every CNPG value itself (kure v0.2.0-beta.13). kure’s
release-2 builder contract retired the CNPG config-struct layer
(cnpg.Cluster/Pooler/ObjectStore/Database and their *Options types) that
postgresql used to go through. The handler then called the generated
CreateCluster/CreatePooler/CreateObjectStore/CreateDatabase and assigned the
upstream cnpgv1 / barman-cloud structs directly. Since go-kure/launcher#281 the
postgresql rule builds those structs as the properties of the cnpg-* kind
components it lowers onto, whose Generate calls the same constructors, and
enablePDB comes from the post-policy step the rule attaches to the Cluster.
The values that layer used to inject are written explicitly, so the emitted
manifests are unchanged byte for byte: enablePDB (true only for more than one
instance), primaryUpdateStrategy: unsupervised, the ACCESS_KEY_ID /
SECRET_ACCESS_KEY key names on backup and objectStore credentials, the
barman-cloud WAL-archiver plugin entry when an objectStore is declared (its
plugin name and parameters were corrected afterwards by go-kure/launcher#643),
the pooler type (rw unless ro was authored) and its always-present
pgbouncer block, and ensure: present on every extension not authored absent.
The layer’s guards are kept too: inheritedMetadata, managed, bootstrap,
postgresql.synchronous and the credential references are omitted when their input
is empty, a pooler instances of zero or less is omitted when nobody authored
it (an authored count is written as authored since go-kure/launcher#790, see
the postgresql entry), and a role’s or database’s ensure /
databaseReclaimPolicy is written only for absent / delete.