Component Handlers

OAM Built-in Component Handlers

Go Reference Go Reference

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:

RuleComponentsTraitsPolicies
webservicedeployment, service, serviceaccounttopology-spread, pvc
workerdeployment, serviceaccounttopology-spread, pvc
helmhelmrelease, helmtemplate, helmrepository, ocirepository, gitrepository, bucketconfigmap, secret
postgresqlcnpg-cluster, cnpg-objectstore, cnpg-pooler, cnpg-databasedependency, 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 of webservice and worker, the Kustomization and the own source of oci, the Cluster and every other member of postgresql. A source helm generates, and a source several oci components 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, worker and an oci with its own source emit one same-name sibling group, which is one bundle: the deployment member carries the trait for the first two, the Kustomization for the third. helm, and an oci on 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:

trait "fluxcd-patches" configures the bundle of every object this component generates, but placement policy "pooler-last" names "db-pooler", a component generated for it, on its own: that component could be applied in a bundle the trait does not reach; name "db" in the policy instead, or remove the trait

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

typeProducesSummary
alertmanagerAlertmanagerKind-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.
apiserviceAPIServiceKind-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.
artifactgeneratorArtifactGeneratorKind-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.
backendtlspolicyBackendTLSPolicyKind-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.
bucketBucketKind-named: the full Flux BucketSpec.
certificateCertificateKind-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-bgpadvertisementCiliumBGPAdvertisementKind-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-bgpclusterconfigCiliumBGPClusterConfigKind-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-bgpnodeconfigoverrideCiliumBGPNodeConfigOverrideKind-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-bgppeerconfigCiliumBGPPeerConfigKind-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-cidrgroupCiliumCIDRGroupKind-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-clusterwidenetworkpolicyCiliumClusterwideNetworkPolicyKind-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-egressgatewaypolicyCiliumEgressGatewayPolicyKind-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-loadbalancerippoolCiliumLoadBalancerIPPoolKind-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-localredirectpolicyCiliumLocalRedirectPolicyKind-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-networkpolicyCiliumNetworkPolicyKind-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-nodeconfigCiliumNodeConfigKind-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.
clusterexternalsecretClusterExternalSecretKind-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.
clusterissuerClusterIssuerKind-named cert-manager ClusterIssuer: the IssuerSpec of issuer, strictly decoded, and its policy check. Cluster-scoped. No capability is required — see below.
clusterroleClusterRoleKind-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.
clusterrolebindingClusterRoleBindingKind-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.
clustersecretstoreClusterSecretStoreKind-named External Secrets Operator ClusterSecretStore: the SecretStoreSpec of secretstore, strictly decoded, and its policy check. Cluster-scoped. No capability is required — see below.
cnpg-backupCNPG BackupKind-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-clusterCNPG ClusterOperator-CR kind component: the whole postgresql.cnpg.io/v1 ClusterSpec, strictly decoded, with no launcher opinions — see below.
cnpg-clusterimagecatalogCNPG ClusterImageCatalogKind-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-databaseCNPG DatabaseOperator-CR kind component: the whole DatabaseSpec, strictly decoded — see below.
cnpg-databaseroleCNPG DatabaseRoleKind-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-imagecatalogCNPG ImageCatalogKind-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-objectstoreBarman Cloud ObjectStoreOperator-CR kind component: the whole barmancloud.cnpg.io/v1 ObjectStoreSpec, strictly decoded — see below.
cnpg-poolerCNPG PoolerOperator-CR kind component: the whole PoolerSpec, strictly decoded — see below.
cnpg-publicationCNPG PublicationKind-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-scheduledbackupCNPG ScheduledBackupKind-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-subscriptionCNPG SubscriptionKind-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.
configmapConfigMapKind-named ConfigMap: data, binaryData, immutable. A workload reads it through a configMap volume or envFrom — see below.
crdCustomResourceDefinition(s)CRDs from inline/url; rejects non-CRD docs.
cronjobCronJobScheduled job; cron schedule + history limits + CronJobSpec/JobSpec fields, plus the raw affinity/tolerations/topologySpreadConstraints (see below).
csidriverCSIDriverKind-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.
daemonsetDaemonSetPer-node daemon; honors tolerations, the raw affinity and topologySpreadConstraints, and takes sidecars. Emits no Service (go-kure/launcher#690).
deploymentDeploymentKind-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.
endpointsliceEndpointSliceKind-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.
externalsecretExternalSecretKind-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-alertAlertKind-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-kustomizationKustomizationKind-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-providerProviderKind-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-receiverReceiverKind-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.
gatewayGatewayKind-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.
gatewayclassGatewayClassKind-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.
gitrepositoryGitRepositoryKind-named: the full Flux GitRepositorySpec.
grpcrouteGRPCRouteKind-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.
helmvia helmrelease (+ a values configmap trait, a secretValues secret trait) + a generated helmrepository/ocirepository/gitrepository/bucket, or via helmtemplateRole-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.
helmchartHelmChartKind-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.
helmreleaseHelmReleaseKind-named: the full Flux HelmReleaseSpec, against an existing source.
helmrepositoryHelmRepositoryKind-named: the full Flux HelmRepositorySpec, and nothing else.
helmtemplaterendered manifestsKind-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.
horizontalpodautoscalerHorizontalPodAutoscalerKind-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.
httprouteHTTPRouteKind-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.
imagepolicyImagePolicyKind-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.
imagerepositoryImageRepositoryKind-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.
imageupdateautomationImageUpdateAutomationKind-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.
ingressIngressKind-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.
ingressclassIngressClassKind-named IngressClass: the whole IngressClassSpec (controller, required, and parameters), strictly decoded. Cluster-scoped; no environment policy applies — see below.
issuerIssuerKind-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.
jobJobRun-to-completion workload; the same JobSpec fields as cronjob’s job template, plus its own suspend and the raw affinity/tolerations/topologySpreadConstraints (see below).
limitrangeLimitRangeKind-named LimitRange: the whole LimitRangeSpec (limits, required), strictly decoded — see below.
listenersetListenerSetKind-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.
manifestsanyRaw manifests from inline/url with namespace stamping + scopeOverrides. Every object is checked against the environment policy — see below.
metallb-bfdprofileBFDProfileKind-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-bgpadvertisementBGPAdvertisementKind-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-bgppeerBGPPeerKind-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-communityCommunityKind-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-ipaddresspoolIPAddressPoolKind-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-l2advertisementL2AdvertisementKind-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.
mutatingwebhookconfigurationMutatingWebhookConfigurationKind-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.
namespaceNamespaceKind-named Namespace: the whole NamespaceSpec (finalizers), strictly decoded. Cluster-scoped, named after the component; its labels are the labels property — see below.
networkpolicyNetworkPolicyKind-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.
ociOCIRepository, KustomizationSync manifests from an OCI artifact (Flux).
ocirepositoryOCIRepositoryKind-named: the full Flux OCIRepositorySpec, with no Kustomization (compare oci).
passthroughany (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.
persistentvolumePersistentVolumeKind-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.
persistentvolumeclaimPersistentVolumeClaimKind-named claim: size, storageClassName, accessModes, volumeMode, selector, dataSourceRef, volumeName, volumeAttributesClassName. A workload mounts it with a pvc volume’s claimName — see below.
podPodKind-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.
poddisruptionbudgetPodDisruptionBudgetKind-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.
podmonitorPodMonitorKind-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.
podtemplatePodTemplateKind-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.
postgresqlCNPG Cluster, Pooler, ObjectStore, DatabaseCloudNativePG database (backup/monitoring/pooling).
priorityclassPriorityClassKind-named PriorityClass: value (0 when unauthored), globalDefault, description, preemptionPolicy, strictly decoded. Cluster-scoped; no environment policy applies — see below.
prometheusPrometheusKind-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-probeProbeKind-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.
prometheusrulePrometheusRuleKind-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.
referencegrantReferenceGrantKind-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.
replicasetReplicaSetKind-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.
replicationcontrollerReplicationControllerKind-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.
replicationdestinationReplicationDestinationKind-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.
replicationsourceReplicationSourceKind-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.
resourcequotaResourceQuotaKind-named ResourceQuota: the whole ResourceQuotaSpec (hard, scopes, scopeSelector), strictly decoded — see below.
resourcesetinputproviderResourceSetInputProviderKind-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.
roleRoleKind-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.
rolebindingRoleBindingKind-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.
runtimeclassRuntimeClassKind-named RuntimeClass: handler (required), overhead, scheduling, strictly decoded. Cluster-scoped; no environment policy applies — see below.
secretSecretKind-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.
secretstoreSecretStoreKind-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.
serviceServiceKind-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.
serviceaccountServiceAccountKind-named ServiceAccount: automountServiceAccountToken, imagePullSecrets. A workload names it with serviceAccountName — see below.
servicecidrServiceCIDRKind-named ServiceCIDR: the whole ServiceCIDRSpec (cidrs, required, at least one), strictly decoded. Cluster-scoped; no environment policy applies — see below.
servicemonitorServiceMonitorKind-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.
statefulsetStatefulSetStateful workload with volumeClaimTemplates; serviceName names a governing service authored beside it. Emits no Service (go-kure/launcher#690).
storageclassStorageClassKind-named StorageClass: the object’s fields beside its identity (provisioner, required, parameters, reclaimPolicy, …), strictly decoded. Cluster-scoped; no environment policy applies — see below.
tcprouteTCPRouteKind-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.
thanosrulerThanosRulerKind-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.
tlsrouteTLSRouteKind-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.
udprouteUDPRouteKind-named Gateway API UDPRoute: the whole UDPRouteSpec, strictly decoded; required as on a tcproute, and an authored object on the same terms — see below.
validatingwebhookconfigurationValidatingWebhookConfigurationKind-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.
volumeattributesclassVolumeAttributesClassKind-named VolumeAttributesClass: driverName and parameters (both required, parameters with at least one entry), strictly decoded. Cluster-scoped; no environment policy applies — see below.
webserviceDeployment, 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.
workerDeployment, 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_CoversEveryConstructor reads 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_MatchesCallSites holds the Status and Type columns to this package and ../traits: a kind row’s constructor is called here, a trait row’s there or here (a trait may build through this package, as configmap does), and a missing, held or not authorable row’s by neither. A kind or component row’s Type is a type a handler or lowering rule of this package declares contract metadata for, a trait row’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 component type.
  • 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 trait type.
  • 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’s service are 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.

ConstructorKindStatusTypeDecodeNotes
kubernetes.CreateAPIServiceapiregistration.k8s.io/v1 APIService (cluster-scoped)kindapiservicestrict decode of APIServiceSpecgroup, 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.CreateBackendTLSPolicygateway.networking.k8s.io/v1 BackendTLSPolicykindbackendtlspolicystrict decode of BackendTLSPolicySpectargetRefs, 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.CreateBindingv1 Bindingnot authorable--A request body for a pod’s binding subresource, not a stored object.
kubernetes.CreateCSIDriverstorage.k8s.io/v1 CSIDriver (cluster-scoped)kindcsidriverstrict decode of CSIDriverSpecThe 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.CreateCSINodestorage.k8s.io/v1 CSINode (cluster-scoped)not authorable--Written by the kubelet for the CSI drivers on its node.
kubernetes.CreateCSIStorageCapacitystorage.k8s.io/v1 CSIStorageCapacitynot authorable--Written by a CSI driver’s provisioner.
kubernetes.CreateClusterRolerbac.authorization.k8s.io/v1 ClusterRole (cluster-scoped)kindclusterrolestrict decode of the object, less kind, apiVersion and metadataUngated: 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.CreateClusterRoleBindingrbac.authorization.k8s.io/v1 ClusterRoleBinding (cluster-scoped)kindclusterrolebindingstrict decode of the object, less kind, apiVersion and metadataUngated: 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.CreateComponentStatusv1 ComponentStatus (cluster-scoped)not authorable--Read-only: the API server computes it.
kubernetes.CreateConfigMapv1 ConfigMapkindconfigmaphand-written parserThe configmap trait builds through the same path.
kubernetes.CreateControllerRevisionapps/v1 ControllerRevisionnot authorable--Written by the StatefulSet and DaemonSet controllers.
kubernetes.CreateCronJobbatch/v1 CronJobkindcronjobhand-written parser-
kubernetes.CreateCustomResourceDefinitionapiextensions.k8s.io/v1 CustomResourceDefinition (cluster-scoped)componentcrdthe manifest parser, CustomResourceDefinition documents onlyThe stated exception: an application takes its CRDs from upstream files (inline or url), so no kind component projects the spec.
kubernetes.CreateDaemonSetapps/v1 DaemonSetkinddaemonsethand-written parser-
kubernetes.CreateDeploymentapps/v1 Deploymentkinddeploymenthand-written parserwebservice and worker lower onto it.
kubernetes.CreateEndpointSlicediscovery.k8s.io/v1 EndpointSlicekindendpointslicestrict decode of the object, less kind, apiVersion and metadataA 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.CreateEndpointsv1 Endpointsnot authorable--Not offered: deprecated upstream in favour of EndpointSlice.
kubernetes.CreateEventv1 Eventnot authorable--A record the system writes at run time.
kubernetes.CreateEvictionpolicy/v1 Evictionnot authorable--A request body for a pod’s eviction subresource, not a stored object.
kubernetes.CreateGRPCRoutegateway.networking.k8s.io/v1 GRPCRoutekindgrpcroutestrict decode of GRPCRouteSpecOn 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.CreateGatewaygateway.networking.k8s.io/v1 Gatewaykindgatewaystrict decode of GatewaySpecgatewayClassName 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.CreateGatewayClassgateway.networking.k8s.io/v1 GatewayClass (cluster-scoped)kindgatewayclassstrict decode of GatewayClassSpeccontrollerName must be written, and of a parametersRef that is authored its group, kind and name. No capability is required. No environment policy applies.
kubernetes.CreateHTTPRoutegateway.networking.k8s.io/v1 HTTPRoutekindhttproutestrict decode of HTTPRouteSpecNo 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.CreateHorizontalPodAutoscalerautoscaling/v2 HorizontalPodAutoscalerkindhorizontalpodautoscalerstrict decode of HorizontalPodAutoscalerSpecscaleTargetRef 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.CreateIPAddressnetworking.k8s.io/v1 IPAddress (cluster-scoped)not authorable--Allocated by the API server for a Service.
kubernetes.CreateIngressnetworking.k8s.io/v1 Ingresskindingressstrict decode of IngressSpecThe 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.CreateIngressClassnetworking.k8s.io/v1 IngressClass (cluster-scoped)kindingressclassstrict decode of IngressClassSpecThe 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.CreateJobbatch/v1 Jobkindjobhand-written parser-
kubernetes.CreateLeasecoordination.k8s.io/v1 Leasenot authorable--Written at run time by its holder: a leader-election client, or the kubelet for its node’s heartbeat.
kubernetes.CreateLimitRangev1 LimitRangekindlimitrangestrict decode of LimitRangeSpec-
kubernetes.CreateListenerSetgateway.networking.k8s.io/v1 ListenerSetkindlistenersetstrict decode of ListenerSetSpecparentRef 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.CreateMutatingAdmissionPolicyadmissionregistration.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.CreateMutatingAdmissionPolicyBindingadmissionregistration.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.CreateMutatingWebhookConfigurationadmissionregistration.k8s.io/v1 MutatingWebhookConfiguration (cluster-scoped)kindmutatingwebhookconfigurationstrict decode of the object, less kind, apiVersion and metadataAs 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.CreateNamespacev1 Namespace (cluster-scoped)kindnamespacestrict decode of NamespaceSpecThe component name is the Namespace’s name. Its labels and annotations are the labels and annotations properties.
kubernetes.CreateNetworkPolicynetworking.k8s.io/v1 NetworkPolicykindnetworkpolicystrict decode of NetworkPolicySpecNo 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.CreateNodev1 Node (cluster-scoped)not authorable--Registered by the kubelet.
kubernetes.CreatePersistentVolumev1 PersistentVolume (cluster-scoped)kindpersistentvolumestrict decode of PersistentVolumeSpecHeld to environment policy on every path that produces one.
kubernetes.CreatePersistentVolumeClaimv1 PersistentVolumeClaimkindpersistentvolumeclaimhand-written parserThe pvc trait builds through the same path.
kubernetes.CreatePodv1 Podkindpodstrict decode of PodSpecHeld to environment policy by the check the rendered paths run on a Pod; ephemeralContainers, priority and overhead refused.
kubernetes.CreatePodDisruptionBudgetpolicy/v1 PodDisruptionBudgetkindpoddisruptionbudgetstrict decode of PodDisruptionBudgetSpecNo 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.CreatePodTemplatev1 PodTemplatekindpodtemplatestrict 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.CreatePriorityClassscheduling.k8s.io/v1 PriorityClass (cluster-scoped)kindpriorityclassstrict decode of the object, less kind, apiVersion and metadataThe 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.CreateRangeAllocationv1 RangeAllocation (cluster-scoped)not authorable--The API server’s own allocation record.
kubernetes.CreateReferenceGrantgateway.networking.k8s.io/v1 ReferenceGrantkindreferencegrantstrict decode of ReferenceGrantSpecfrom 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.CreateReplicaSetapps/v1 ReplicaSetkindreplicasetstrict decode of ReplicaSetSpecHeld 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.CreateReplicationControllerv1 ReplicationControllerkindreplicationcontrollerstrict decode of ReplicationControllerSpecHeld 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.CreateResourceQuotav1 ResourceQuotakindresourcequotastrict decode of ResourceQuotaSpec-
kubernetes.CreateRolerbac.authorization.k8s.io/v1 Rolekindrolestrict decode of the object, less kind, apiVersion and metadataUngated: 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.CreateRoleBindingrbac.authorization.k8s.io/v1 RoleBindingkindrolebindingstrict decode of the object, less kind, apiVersion and metadataUngated: 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.CreateRuntimeClassnode.k8s.io/v1 RuntimeClass (cluster-scoped)kindruntimeclassstrict decode of the object, less kind, apiVersion and metadataThe 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.CreateSecretv1 Secretkindsecrethand-written parserThe 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.CreateServicev1 Servicekindservicehand-written parser-
kubernetes.CreateServiceAccountv1 ServiceAccountkindserviceaccounthand-written parser-
kubernetes.CreateServiceCIDRnetworking.k8s.io/v1 ServiceCIDR (cluster-scoped)kindservicecidrstrict decode of ServiceCIDRSpecThe 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.CreateStatefulSetapps/v1 StatefulSetkindstatefulsethand-written parser-
kubernetes.CreateStorageClassstorage.k8s.io/v1 StorageClass (cluster-scoped)kindstorageclassstrict decode of the object, less kind, apiVersion and metadataThe 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.CreateTCPRoutegateway.networking.k8s.io/v1 TCPRoutekindtcproutestrict decode of TCPRouteSpecNo 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.CreateTLSRoutegateway.networking.k8s.io/v1 TLSRoutekindtlsroutestrict decode of TLSRouteSpecAs 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.CreateUDPRoutegateway.networking.k8s.io/v1 UDPRoutekindudproutestrict decode of UDPRouteSpecAs 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.CreateValidatingAdmissionPolicyadmissionregistration.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.CreateValidatingAdmissionPolicyBindingadmissionregistration.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.CreateValidatingWebhookConfigurationadmissionregistration.k8s.io/v1 ValidatingWebhookConfiguration (cluster-scoped)kindvalidatingwebhookconfigurationstrict decode of the object, less kind, apiVersion and metadataThe 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.CreateVolumeAttachmentstorage.k8s.io/v1 VolumeAttachment (cluster-scoped)not authorable--Written by the attach/detach controller.
kubernetes.CreateVolumeAttributesClassstorage.k8s.io/v1 VolumeAttributesClass (cluster-scoped)kindvolumeattributesclassstrict decode of the object, less kind, apiVersion and metadataThe 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.CreateCertificatecert-manager.io/v1 Certificatekindcertificatestrict decode of CertificateSpecIts 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.CreateCertificateRequestcert-manager.io/v1 CertificateRequestnot authorable--A one-shot request cert-manager creates for a Certificate.
certmanager.CreateChallengeacme.cert-manager.io/v1 Challengenot authorable--Created by cert-manager’s ACME issuer.
certmanager.CreateClusterIssuercert-manager.io/v1 ClusterIssuer (cluster-scoped)kindclusterissuerstrict decode of IssuerSpecIts 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.CreateIssuercert-manager.io/v1 Issuerkindissuerstrict decode of IssuerSpecAs clusterissuer, in the build namespace. Its labels and annotations are the labels and annotations properties.
certmanager.CreateOrderacme.cert-manager.io/v1 Ordernot authorable--Created by cert-manager’s ACME issuer.
cilium.CreateCiliumBGPAdvertisementcilium.io/v2 CiliumBGPAdvertisement (cluster-scoped)kindcilium-bgpadvertisementstrict decode of CiliumBGPAdvertisementSpecThe 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.CreateCiliumBGPClusterConfigcilium.io/v2 CiliumBGPClusterConfig (cluster-scoped)kindcilium-bgpclusterconfigstrict decode of CiliumBGPClusterConfigSpecThe 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.CreateCiliumBGPNodeConfigcilium.io/v2 CiliumBGPNodeConfig (cluster-scoped)not authorable--Generated by the Cilium operator from a CiliumBGPClusterConfig.
cilium.CreateCiliumBGPNodeConfigOverridecilium.io/v2 CiliumBGPNodeConfigOverride (cluster-scoped)kindcilium-bgpnodeconfigoverridestrict decode of CiliumBGPNodeConfigOverrideSpecThe 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.CreateCiliumBGPPeerConfigcilium.io/v2 CiliumBGPPeerConfig (cluster-scoped)kindcilium-bgppeerconfigstrict decode of CiliumBGPPeerConfigSpecThe 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.CreateCiliumCIDRGroupcilium.io/v2 CiliumCIDRGroup (cluster-scoped)kindcilium-cidrgroupstrict decode of CiliumCIDRGroupSpecThe 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.CreateCiliumClusterwideEnvoyConfigcilium.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.CreateCiliumClusterwideNetworkPolicycilium.io/v2 CiliumClusterwideNetworkPolicy (cluster-scoped)kindcilium-clusterwidenetworkpolicystrict decode of spec and specs into the Cilium RuleThe 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.CreateCiliumEgressGatewayPolicycilium.io/v2 CiliumEgressGatewayPolicy (cluster-scoped)kindcilium-egressgatewaypolicystrict decode of CiliumEgressGatewayPolicySpecThe 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.CreateCiliumEndpointcilium.io/v2 CiliumEndpointnot authorable--Written by the Cilium agent.
cilium.CreateCiliumEnvoyConfigcilium.io/v2 CiliumEnvoyConfigheld--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.CreateCiliumIdentitycilium.io/v2 CiliumIdentity (cluster-scoped)not authorable--Written by Cilium when it allocates an identity.
cilium.CreateCiliumLoadBalancerIPPoolcilium.io/v2 CiliumLoadBalancerIPPool (cluster-scoped)kindcilium-loadbalancerippoolstrict decode of CiliumLoadBalancerIPPoolSpecThe 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.CreateCiliumLocalRedirectPolicycilium.io/v2 CiliumLocalRedirectPolicykindcilium-localredirectpolicystrict decode of CiliumLocalRedirectPolicySpecThe 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.CreateCiliumNetworkPolicycilium.io/v2 CiliumNetworkPolicykindcilium-networkpolicystrict decode of spec and specs into the Cilium RuleThe 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.CreateCiliumNodecilium.io/v2 CiliumNode (cluster-scoped)not authorable--Written by the Cilium agent for its node.
cilium.CreateCiliumNodeConfigcilium.io/v2 CiliumNodeConfigkindcilium-nodeconfigstrict decode of CiliumNodeConfigSpecThe 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.CreateBackuppostgresql.cnpg.io/v1 Backupkindcnpg-backupstrict decode of BackupSpecThe 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.CreateClusterpostgresql.cnpg.io/v1 Clusterkindcnpg-clusterstrict decode of ClusterSpecpostgresql lowers onto it.
cnpg.CreateClusterImageCatalogpostgresql.cnpg.io/v1 ClusterImageCatalog (cluster-scoped)kindcnpg-clusterimagecatalogstrict decode of ImageCatalogSpecAs cnpg-imagecatalog, with no namespace. Its labels and annotations are the labels and annotations properties.
cnpg.CreateDatabasepostgresql.cnpg.io/v1 Databasekindcnpg-databasestrict decode of DatabaseSpecpostgresql lowers onto it.
cnpg.CreateDatabaseRolepostgresql.cnpg.io/v1 DatabaseRolekindcnpg-databaserolestrict decode of DatabaseRoleSpecThe 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.CreateFailoverQuorumpostgresql.cnpg.io/v1 FailoverQuorumnot authorable--Written by the CloudNativePG operator.
cnpg.CreateImageCatalogpostgresql.cnpg.io/v1 ImageCatalogkindcnpg-imagecatalogstrict decode of ImageCatalogSpecThe 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.CreateObjectStorebarmancloud.cnpg.io/v1 ObjectStorekindcnpg-objectstorestrict decode of ObjectStoreSpecpostgresql lowers onto it.
cnpg.CreatePoolerpostgresql.cnpg.io/v1 Poolerkindcnpg-poolerstrict decode of PoolerSpecpostgresql lowers onto it.
cnpg.CreatePublicationpostgresql.cnpg.io/v1 Publicationkindcnpg-publicationstrict decode of PublicationSpecThe 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.CreateScheduledBackuppostgresql.cnpg.io/v1 ScheduledBackupkindcnpg-scheduledbackupstrict decode of ScheduledBackupSpecThe 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.CreateSubscriptionpostgresql.cnpg.io/v1 Subscriptionkindcnpg-subscriptionstrict decode of SubscriptionSpecThe 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.CreateClusterExternalSecretexternal-secrets.io/v1 ClusterExternalSecret (cluster-scoped)kindclusterexternalsecretstrict decode of ClusterExternalSecretSpecThe 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.CreateClusterSecretStoreexternal-secrets.io/v1 ClusterSecretStore (cluster-scoped)kindclustersecretstorestrict decode of SecretStoreSpecThe 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.CreateExternalSecretexternal-secrets.io/v1 ExternalSecretkindexternalsecretstrict decode of ExternalSecretSpecIts 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.CreateSecretStoreexternal-secrets.io/v1 SecretStorekindsecretstorestrict decode of SecretStoreSpecAs clustersecretstore, in the build namespace. Its labels and annotations are the labels and annotations properties.
fluxcd.CreateAlertnotification.toolkit.fluxcd.io/v1beta3 Alertkindfluxcd-alertstrict decode of AlertSpecproviderRef 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.CreateArtifactGeneratorsource.extensions.fluxcd.io/v1beta1 ArtifactGeneratorkindartifactgeneratorstrict decode of ArtifactGeneratorSpecsources 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.CreateBucketsource.toolkit.fluxcd.io/v1 Bucketkindbucketstrict decode of BucketSpec-
fluxcd.CreateExternalArtifactsource.toolkit.fluxcd.io/v1 ExternalArtifactnot authorable--Written by the controller that produces the artifact.
fluxcd.CreateFluxInstancefluxcd.controlplane.io/v1 FluxInstanceheld--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.CreateFluxReportfluxcd.controlplane.io/v1 FluxReportnot authorable--Written by the Flux operator.
fluxcd.CreateGitRepositorysource.toolkit.fluxcd.io/v1 GitRepositorykindgitrepositorystrict decode of GitRepositorySpec-
fluxcd.CreateHelmChartsource.toolkit.fluxcd.io/v1 HelmChartkindhelmchartstrict decode of HelmChartSpec-
fluxcd.CreateHelmReleasehelm.toolkit.fluxcd.io/v2 HelmReleasekindhelmreleasestrict decode of HelmReleaseSpechelm lowers onto it.
fluxcd.CreateHelmRepositorysource.toolkit.fluxcd.io/v1 HelmRepositorykindhelmrepositorystrict decode of HelmRepositorySpec-
fluxcd.CreateImagePolicyimage.toolkit.fluxcd.io/v1 ImagePolicykindimagepolicystrict decode of ImagePolicySpecimageRepositoryRef 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.CreateImageRepositoryimage.toolkit.fluxcd.io/v1 ImageRepositorykindimagerepositorystrict decode of ImageRepositorySpecimage 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.CreateImageUpdateAutomationimage.toolkit.fluxcd.io/v1 ImageUpdateAutomationkindimageupdateautomationstrict decode of ImageUpdateAutomationSpecsourceRef 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.CreateKustomizationkustomize.toolkit.fluxcd.io/v1 Kustomizationkindfluxcd-kustomizationstrict decode of KustomizationSpecoci lowers onto it. targetNamespace is never defaulted.
fluxcd.CreateOCIRepositorysource.toolkit.fluxcd.io/v1 OCIRepositorykindocirepositorystrict decode of OCIRepositorySpecoci lowers onto it.
fluxcd.CreateProvidernotification.toolkit.fluxcd.io/v1beta3 Providerkindfluxcd-providerstrict decode of ProviderSpectype 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.CreateReceivernotification.toolkit.fluxcd.io/v1 Receiverkindfluxcd-receiverstrict decode of ReceiverSpectype 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.CreateResourceSetfluxcd.controlplane.io/v1 ResourceSetheld--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.CreateResourceSetInputProviderfluxcd.controlplane.io/v1 ResourceSetInputProviderkindresourcesetinputproviderstrict decode of ResourceSetInputProviderSpectype 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.CreateBFDProfilemetallb.io/v1beta1 BFDProfilekindmetallb-bfdprofilestrict decode of BFDProfileSpecThe 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.CreateBGPAdvertisementmetallb.io/v1beta1 BGPAdvertisementkindmetallb-bgpadvertisementstrict decode of BGPAdvertisementSpecThe 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.CreateBGPPeermetallb.io/v1beta2 BGPPeerkindmetallb-bgppeerstrict decode of BGPPeerSpecThe 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.CreateCommunitymetallb.io/v1beta1 Communitykindmetallb-communitystrict decode of CommunitySpecThe 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.CreateConfigurationStatemetallb.io/v1beta1 ConfigurationStatenot authorable--Status MetalLB writes.
metallb.CreateIPAddressPoolmetallb.io/v1beta1 IPAddressPoolkindmetallb-ipaddresspoolstrict decode of IPAddressPoolSpecThe 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.CreateL2Advertisementmetallb.io/v1beta1 L2Advertisementkindmetallb-l2advertisementstrict decode of L2AdvertisementSpecThe 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.CreateServiceBGPStatusmetallb.io/v1beta1 ServiceBGPStatusnot authorable--Status MetalLB writes.
metallb.CreateServiceL2Statusmetallb.io/v1beta1 ServiceL2Statusnot authorable--Status MetalLB writes.
prometheus.CreateAlertmanagermonitoring.coreos.com/v1 Alertmanagerkindalertmanagerstrict decode of AlertmanagerSpecHeld 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.CreatePodMonitormonitoring.coreos.com/v1 PodMonitorkindpodmonitorstrict decode of PodMonitorSpecselector must be written, and the three required fields of an endpoint’s oauth2. No environment policy applies, and no capability is required.
prometheus.CreateProbemonitoring.coreos.com/v1 Probekindprometheus-probestrict decode of ProbeSpecprober.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.CreatePrometheusmonitoring.coreos.com/v1 Prometheuskindprometheusstrict decode of PrometheusSpecHeld 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.CreatePrometheusRulemonitoring.coreos.com/v1 PrometheusRulekindprometheusrulestrict decode of PrometheusRuleSpecA group’s name and a rule’s expr must be written. No environment policy applies, and no capability is required.
prometheus.CreateServiceMonitormonitoring.coreos.com/v1 ServiceMonitorkindservicemonitorstrict decode of ServiceMonitorSpecendpoints 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.CreateThanosRulermonitoring.coreos.com/v1 ThanosRulerkindthanosrulerstrict decode of ThanosRulerSpec; its labels is the externalLabels propertyHeld 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.CreateReplicationDestinationvolsync.backube/v1alpha1 ReplicationDestinationkindreplicationdestinationstrict decode of ReplicationDestinationSpecAs replicationsource, without a Syncthing mover. Its labels and annotations are the labels and annotations properties.
volsync.CreateReplicationSourcevolsync.backube/v1alpha1 ReplicationSourcekindreplicationsourcestrict decode of ReplicationSourceSpecNo 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.

ListWhereHeld in order by
ComponentHandlerspkg/oam/builtin/registry/registry.goTestKindLists_InOrder (pkg/cmd/kurel)
validComponentTypespkg/oam/validate.goTestKindLists_InOrder
wantHandlers, componentLabelFixturesthe tests of pkg/cmd/kurelTestKindLists_InOrder
coreKindSchemas, apiSetKindsthe tests of this packageTestKindLists_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.gonot 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 policyFreeKindthe tests of this packageTestKindLists_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 APIthe tests of this packageTestKindLists_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 imageimage_fields_internal_test.goTestKindLists_InOrder, in the order of the entries’ name
kindTraitPairs, for a kind a trait also generatesthe tests of pkg/cmd/kurelTestKindLists_InOrder, in the order of the entries’ typ
policyFreeTypes, for a kind built on policyFreeKind whose type publishes its field commentskind_policy_free_internal_test.gono 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 fileTestKindLists_InOrder
“Component type allowlist”pkg/oam/README.mdTestKindLists_InOrder; TestKindLists_Complete fails on a type of validComponentTypes with no row and on a row of no type of it
Table 4.2docs/oam/design-kurel-package.mdTestKindLists_InOrder
The table of kind components under “kurel build”pkg/cmd/kurel/README.mdTestKindLists_InOrder
“Per-type highlights”this fileno 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 kindsdocs/delivery-scope.mdno 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 gofmt aligns (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 (missing to kind): two changes that edit neighbouring rows;
  • the entries of §6.2 of docs/delivery-scope.md that 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.

PropertyTypeEffect
imagePullPolicystring: Always, IfNotPresent, NeverimagePullPolicy of the container.
terminationMessagePathstringterminationMessagePath, passed through: upstream validates no path shape for it, so none is invented here.
terminationMessagePolicystring: File, FallbackToLogsOnErrorterminationMessagePolicy.
stdin, stdinOnce, ttybooleanThe field of that name. Upstream has no rule relating the three, so none is applied.
resizePolicylist 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 imagePullPolicy see Every spec field is this package’s to write). An authored false on a boolean is the field’s zero value and renders as absent too.
  • No policy check. imagePullPolicy: Never and IfNotPresent are accepted with no capability gate and no Policy method: a platform that requires Always has 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 empty resizePolicy list. An empty string on imagePullPolicy or terminationMessagePolicy is 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: always is refused with imagePullPolicy: invalid value "always", must be one of Always, IfNotPresent, Never.
  • The resizePolicy rules are upstream’s (validateResizePolicy, validateInitContainers in k8s.io/kubernetes pkg/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; …);
    • RestartContainer is refused on an initContainers entry: Kubernetes allows it only on a restartable init container, which this package does not model. NotRequired is accepted there;
    • on a pod whose restartPolicy is Never — which only job and cronjob can author — every container’s restart policy must be NotRequired. That check runs on the assembled pod spec (checkResizePolicyRestart), so it is reported at Generate, not at ToApplicationConfig.
  • On an initContainers or sidecars entry 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:

  • restartPolicy is Never or OnFailure, overriding the pod’s restart policy for that container. Always is refused with its reason: it makes the init container restartable (a native sidecar), which this package does not model, the same reason probes and lifecycle are refused on an init entry; author a sidecars entry instead;
  • restartPolicyRules needs restartPolicy and holds at most 20 rules. A rule is a closed object: action is required and is Restart (upstream’s RestartAllContainers needs a separate feature gate, and is refused), and exitCodes is required: a closed object whose operator is required and is In or NotIn, and whose optional values is 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 ContainerRestartRules feature 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”).

  • claimName must be a DNS-1123 subdomain.
  • size and storageClass are 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.
  • claimObjectName is refused alongside it: it names the claim a webservice or worker volume generates, and this volume generates none.
  • accessModes stays: it states the referenced claim’s modes, defaulting to ReadWriteOnce as on a claim a role kind describes. The workload still reads them. A non-RWX claim still forces strategy: Recreate on a Deployment, still refuses replicas above 1, and still limits a scaler to one replica. To scale against a ReadWriteMany claim, state accessModes: [ReadWriteMany] on the volume.
  • volumeMode, readOnly and 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. Adds volumeMode (Filesystem|Block) and devicePath. The entry takes exactly one of mountPath and devicePath, and devicePath is authored if and only if volumeMode: Block is — in both directions, with nothing inferred: mountPath with Block, devicePath with Filesystem or with no mode, and both paths at once are each an error. A Block entry reaches the main container as a corev1.VolumeDevice and has no volumeMount. The claim carries the authored mode; an unauthored mode stays unset (the apiserver defaults it to Filesystem), so an existing document’s claim is byte-identical. devicePath on any other volume type is an error.
  • volumeClaimTemplates[] entry (statefulset). The same exactly-one rule with devicePath, and volumeMode: Block is now accepted (it was rejected before). A Block template renders a claim template with volumeMode: Block and a main-container volumeDevices entry.
  • The published schema says “exactly one of”. A volumes[] entry and a volumeClaimTemplates[] entry publish mountPath and devicePath as two optional keys plus a required exclusive group 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 a devicePath is both, through the transform and through a direct handler call alike.
  • initContainers[] / sidecars[] entry. Adds volumeDevices: [{name, devicePath}], a closed key set with both keys required. A volumeDevices name must be a Block volume this component declares (a volumes pvc entry or a claim template); a volumeMounts name must not be one. A volumeMounts name the component does not declare at all stays accepted, because the configmap and external-secret traits 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 in volumeDevices and not in both volumeMounts and volumeDevices; a devicePath is unique, contains no .. element, and is not also a mountPath. On statefulset this spans the claim templates and volumes together, so a Block claim template cannot share its name with a volumes entry or another claim template. The configmap and external-secret traits 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 a devicePath.

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.

PropertyTypeEffectKind
serviceAccountNamestringBehavior-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
automountServiceAccountTokenboolPod-level token automount override.additive
terminationGracePeriodSecondsint ≥ 0Grace period before SIGKILL.additive
podActiveDeadlineSecondsint 1..MaxInt32Pod-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, dnsConfigenum, objectClusterFirstWithHostNet/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}schedulingPlacement 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, hostIPCboolHost 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
shareProcessNamespaceboolMutually exclusive with hostPID: true.additive
hostname, subdomain, setHostnameAsFQDN, hostnameOverride, hostAliases[]{ip,hostnames}naminghostname/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
podSecurityContextobjectThe 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}miscpodResources 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 ports under the name port gave it, so named probe and hook ports keep resolving: port: N becomes ports: [{name: http, containerPort: N}] on daemonset, ports: [{name: tcp, containerPort: N}] on statefulset.
  • Author the Service as a service component that selects the pods (selector: {app: <component>}). For a statefulset’s governing Service, set clusterIP: None and name it in serviceName.
  • serviceName no longer defaults to the component name: unset, the StatefulSet carries serviceName: "". The API server refuses a change to spec.serviceName on an existing StatefulSet, so one that relied on the default and now names the authored Service must be recreated: delete it with --cascade=orphan so its pods survive, then apply. The adopted pods keep the old spec.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: run kubectl rollout restart statefulset/<name> (or, under updateStrategy: OnDelete, delete the pods one at a time). A rollingUpdate.partition excludes the pods below it from the restart: set it to 0 for 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, httproute or expose backend, or a networkPolicy ingress allow) now belongs on the authored service; on the workload it is refused with component "<name>" has no service port.

examples/13-statefulset.yaml shows the result.

propertytypenotescompat
portsarrayEach 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):

kindaffinitytolerationstopologySpreadConstraints
deploymentrawyesyes
statefulsetrawyesyes
daemonsetrawyesyes
jobrawyesyes
cronjobrawyesyes

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 labelSelector names. The pods a kind builds carry app: <component> (see The app label).
  • No policy check. tolerations is accepted with no capability gate and no Policy method 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-spread trait stays Deployment-only. It is refused on a statefulset, a daemonset, a job and a cronjob, with or without authored constraints (component "<name>" generates no Deployment the trait can act on).
  • On a cronjob the 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 daemonset the 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 a DoNotSchedule spread constraint the node cannot satisfy leaves that pod pending instead of placing it elsewhere. Nothing here checks for that.
propertytypenotescompat
affinityobjectnodeAffinity, 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
tolerationsarrayThe 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
topologySpreadConstraintsarraymaxSkew (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:

changeeffect on daemonsetwhy it is kept rather than gated to deployment
A well-formed tolerationSeconds is now readAdditive 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 errorNew 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 acceptedPurely 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 errorNew 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 errorNew 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 errorNew 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 omissionFewer 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 errorNew 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 errorNew 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 errorNew 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 errorNew 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:

  1. It was refused on apply anyway. An empty key with a non-Exists operator, a value under Exists, a non-qualified non-empty key, an invalid label value under Equal. Nothing appliable is lost.
  2. Part of it was being discarded unread. An unrecognised member inside an entry, tolerations authored as a mapping rather than an array, or a tolerationSeconds the parser never looked at — malformed, or well-formed but carrying an effect other than NoExecute.

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.

PropertyTypeEffectCompatibility
strategyobjecttype (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.maxUnavailableint ≥ 0 or percentage stringSame 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.maxSurgeint ≥ 0 or percentage stringSame 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
minReadySecondsint ≥ 0Seconds 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
revisionHistoryLimitint ≥ 0Old ReplicaSets retained. 0 is meaningful (retain none) and distinguishable from unset.additive
pausedboolPauses rollouts of the Deployment.additive
progressDeadlineSecondsint ≥ 0Must 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 build already 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 ValidateAuthoredProperties used to get one replica for replicas: "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:

  1. Authored — whatever the component’s resources property set explicitly.
  2. Policy default — ApplyPolicy fills any request/limit the author left unset from oam.Policy.DefaultCPURequest()/DefaultMemoryRequest()/ DefaultCPULimit()/DefaultMemoryLimit() (enforce.go’s applyDefaultQuantity). A negative policy default fails the build, as a negative authored quantity does, since Kubernetes would refuse it at apply; zero is accepted.
  3. Intrinsic handler default — buildResourceRequirements (common.go) fills anything still unset at Generate() 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.RefusalPrivileged on a workload kind’s own ApplyPolicy, on an operator CR’s pod template, on an object a chart renders, and on a passthrough or manifests object, 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.RefusalResourceMaximum for a cpu or memory request or limit (a container’s, a pod’s, the block a CloudNativePG kind, an alertmanager, a thanosruler or a prometheus carries, and the pod template of an ACME HTTP01 solver on an issuer or a clusterissuer), oam.RefusalStorageMaximum for a claim’s request, a claim template’s, a generic ephemeral volume’s, a PersistentVolume’s capacity and a cnpg-cluster volume, the 1Gi fallback of postgresql included, which its post-policy step holds to the maximum. oam.RefusalReplicaMaximum covers replicas, a HorizontalPodAutoscaler’s maxReplicas and cnpg-cluster’s instances.
  • Secret material in the document is oam.RefusalExplicitSecret: a secret component, a Secret that passthrough or a manifests source carries, helmtemplate’s secretValues, a certificate’s keystore password (keystores.jks.password, keystores.pkcs12.password), and in a secretstore or a clustersecretstore a credential written as a value and the data of the fake provider.
  • A source host is oam.RefusalRegistry, as an image’s registry is: the url of a manifests or crd source, a helmtemplate chart source, the Flux source kinds, an oci:// url that does not name its registry, and a bucket endpoint on Amazon S3.
  • An object that cannot be read is oam.RefusalUnreadableObject on 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 the passthrough and manifests entries.
  • Unclassified, though it is the component’s violation: the rules that hold with or without a policy and are checked at the policy step (ephemeralContainers and 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 (an items array on a kind that is no list, for one).
  • A redirect of a manifests url source to a host outside the allowed registries is a fetch failure, with the error a fetch failure has and no oam.ViolationError; that error holds the oam.PolicyRefusal of class oam.RefusalRegistry, which errors.As reaches.

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:

if pod, prefix, ok := components.MonitoringPodSpec(obj); ok {
	// pod.Containers[0].SecurityContext is at <prefix>.containers[0].securityContext in obj.
	scanPod(pod, prefix)
}
  • What it holds: the fields the kind’s policy checks read, from the same mapping: the listed containers and initContainers, volumes, securityContext, hostNetwork where the spec has it (a ThanosRuler’s has none), dnsPolicy, and the nameservers of dnsConfig. 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 searches and options of dnsConfig are not in the view either.
    KindGenerated containersGenerated init containers
    alertmanageralertmanager, config-reloaderinit-config-reloader
    prometheusprometheus, config-reloader, and thanos-sidecar where thanos is setinit-config-reloader
    thanosrulerthanos-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/v1 APIService and an admissionregistration.k8s.io/v1 ValidatingWebhookConfiguration and MutatingWebhookConfiguration. Each emits that one object, named after the component unless objectName names it, with no namespace, holding exactly what was authored, on policyFreeKind. 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 APIServiceSpec strictly. From the markers, groupPriorityMinimum and versionPriority are required, which the type writes unauthored. By hand, from the API server’s validation: version must be written, group too (the API exempts v1, the core API, whose APIService is named v1.; that is no DNS-1123 subdomain, so no component name or objectName can give it, and the kind requires the group), and an authored service its namespace and name. 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 how caBundle and insecureSkipTLSVerify combine are left to the API server. caBundle is base64, as the API’s JSON writes bytes.

    validatingwebhookconfiguration and mutatingwebhookconfiguration have no spec: their one property is the object’s webhooks, strictly decoded, and kind, apiVersion and metadata are refused. From the markers, a webhook’s name, clientConfig, sideEffects and admissionReviewVersions, a Service’s namespace and name, and a match condition’s name and expression must be written. By hand, from the API server’s validation at Kubernetes v1.37.1: clientConfig holds exactly one of url and service (webhooks[0].clientConfig: exactly one of url and service is required …), admissionReviewVersions holds 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’s namespaceSelector and objectSelector are 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 of failurePolicy, matchPolicy, sideEffects, reinvocationPolicy and 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 authored webhooks: [] 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 unless objectName names it; the handler adds no label, no annotation and no default. The type is fluxcd-alert rather than alert, which beside a prometheusrule would 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 ApplyPolicy checks neither here, as it checks neither on helmrelease (its chart.spec.sourceRef.namespace, chartRef.namespace, serviceAccountName and kubeConfig) and on fluxcd-kustomization (its sourceRef.namespace, serviceAccountName and kubeConfig). 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 providerRef may name another application’s Provider, a Receiver’s resources another application’s Flux objects, an ImagePolicy’s imageRepositoryRef another application’s ImageRepository, the source of an ImageUpdateAutomation or of an ArtifactGenerator another application’s source, a ResourceSetInputProvider’s selector without a namespace another application’s ExternalArtifacts, and an ImageUpdateAutomation without a policySelector takes 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 says accessFrom is for. Nothing gates this either, and no namespace has to be written for it.

    The fields, per kind:

    • fluxcd-alert: eventSources[].namespace names the namespace of the objects whose events are sent, and a source’s name: "*" takes every object of its kind there, or, with matchLabels, every one that carries those labels. The events go to the Provider providerRef names, so an Alert sends what happens to another namespace’s Flux objects to a receiver its author chose. providerRef holds 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 to address, which the API calls “the endpoint, in a generic sense, to where alerts are sent” (for some types “a project ID or a namespace”), through proxy or the proxy the Secret of proxySecretRef holds. Both are hosts outside the cluster unless the receiver runs in it, and launcher writes both as authored. serviceAccountName names 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 types azureeventhub, azuredevops and googlepubsub, and only with the controller’s ObjectLevelWorkloadIdentity feature 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 its type names (“the validation procedure and payload deserialization”), makes the controller reconcile the objects resources names. resources[].namespace names the namespace of the objects a webhook makes the controller reconcile, and a resource’s name: "*" takes every object of its kind there, or, with matchLabels, 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 Secret secretRef names, one of the namespace the Receiver lands in; a generic-oidc Receiver validates a request by the OIDC issuers of oidcProviders instead. oidcProviders[].issuerURL names 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.namespace names 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, an accessFrom list “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 of image at the registry that name holds, which is a host outside the cluster unless the registry runs in it. accessFrom opens 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”, its namespaceSelectors are joined by OR, and “an empty map of MatchLabels matches all namespaces in a cluster”, so accessFrom: {namespaceSelectors: [{}]} opens the scanned tags to an ImagePolicy of any namespace. serviceAccountName names 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: proxySecretRef names 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.namespace names 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 or git.checkout.ref names, or to git.push.branch and git.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 (policySelector narrows them; it names no other namespace), and the Secret of git.commit.signingKey is one of that namespace too. In the Flux namespace, an ImageUpdateAutomation without a policySelector selects 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[].namespace names 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 its type names for input sets, which, in the API’s words, “populate the inputs” of a ResourceSet; the answer decides what that ResourceSet deploys. Every type but Static and ExternalArtifact asks the host of url (Policy, below), with the credentials of the Secret secretRef names; an ExternalService provider sends that Secret’s token or user and password to it. An ExternalArtifact provider reads ExternalArtifacts of the cluster instead: selectors[].namespace names the namespace it lists them in, and namespace: "*" lists them “across all namespaces”. Left out, the namespace is the provider’s own. serviceAccountName names 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 an ExternalArtifact provider, 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 for eventMetadata) and suspend.
    • 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 for proxySecretRef), proxySecretRef, secretRef, certSecretRef (each a name), serviceAccountName, suspend and commitStatusExpr. commitStatusExpr is 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 an apiVersion, kind, name, namespace, matchLabels and filter), resourceFilter, secretRef (a name), oidcProviders (each an issuerURL, audience, variables and validations) and suspend. resourceFilter, a resource’s filter, and the expression of 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, interval and suspend. An authored filterTags is written with both its pattern and its extract, the one left out as the empty string: the Go type omits neither.
    • imagerepository (ImageRepositorySpec): image, interval, timeout, secretRef, proxySecretRef, certSecretRef (each a name), serviceAccountName, suspend, accessFrom (namespaceSelectors, each a matchLabels), exclusionList, provider and insecure. image names a repository, such as ghcr.io/org/app; launcher does not parse it beyond its registry (Policy, below), and an entry of exclusionList is a regular expression it does not compile.
    • imageupdateautomation (ImageUpdateAutomationSpec): sourceRef, git (checkout, commit, push), interval, policySelector, update and suspend. git.commit.messageTemplate is a template the controller renders; launcher writes it as authored and does not parse it.
    • artifactgenerator (ArtifactGeneratorSpec): commonMetadata, sources, pathPattern and artifacts (each a name, revision, originRevision and copy; a copy a from, to, exclude and strategy). commonMetadata is not the object’s own metadata: its labels and annotations are 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 the labels and annotations properties. At generation they are held to the reserved metadata keys (pkg/oam README, “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 a name), insecure, defaultValues, filter (includeBranch, excludeBranch, includeTag, excludeTag, includeEnvironment, excludeEnvironment, labels, limit and semver), skip (labels), schedule (each a cron, timeZone and window) and selectors (each a name, namespace, matchLabels and matchExpressions). defaultValues is a free map, written as authored; the regular expressions of filter, its semver range and a schedule’s cron are written as authored and not parsed.
    • No default is filled. The API’s own (eventSeverity: info, digestReflectionPolicy: Never, a policy’s order: asc, update: {strategy: Setters}, an ImageRepository’s provider: generic and its exclusionList of ^.*\.sig$, a Receiver’s interval: 10m, a ResourceSetInputProvider’s filter.limit: 100 and a schedule’s timeZone: UTC) is applied by the API server to what the object leaves out; the timeout of an ImageRepository left out is, in the API’s words, the “‘Interval’ duration”, and the audience of a Receiver’s OIDC provider notification-controller, which are the controller’s to apply. A schedule’s window left out is written 0s, the API’s own default. One default cannot apply: sourceRef.kind, which the API defaults to GitRepository and 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’s order, update.strategy, an ImageRepository’s provider and a schedule’s timeZone are 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’s filter.limit: 0 is 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_DefaultedZeros holds 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 empty exclusionList ([]) 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, m and h. 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.5ms as 500µs). It is written as Go formats it (10m as 10m0s). TestFluxKinds_DurationsMatchMarkers holds each kind’s list of durations to its type and to the pattern markers of the linked source: the interval of a Receiver, of an ImagePolicy and of an ImageUpdateAutomation, and the interval and timeout of a Provider and of an ImageRepository, and the window of a ResourceSetInputProvider’s schedule; an Alert and an ArtifactGenerator have none. A schedule’s window is held by its authored text: it sits under a list, and a window of 500us in any schedule is refused (resourcesetinputprovider: schedule[].window "500us" is invalid: must be a Flux duration (…)); its pattern takes h, so one is written as Go formats it (90m as 1h30m0s). The timeout of a Provider and of an ImageRepository takes no h: its pattern holds the units ms, s and m, so 1h is refused (imagerepository: timeout "1h" is invalid: must be a Flux duration (…)) and 60m is not. Go formats an hour or more with an h, which the pattern would refuse at apply, so such a timeout is written in minutes (90m as 90m0s), 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: providerRef with its name, and eventSources; of each source its kind and name.
    • A fluxcd-provider: type; of an authored secretRef, proxySecretRef or certSecretRef its name.
    • A fluxcd-receiver: type and resources; of each resource its kind and name; of an authored secretRef its name; of each OIDC provider its issuerURL and validations, of each validation its expression and message, of each variable its name and expression.
    • An imagepolicy: imageRepositoryRef with its name, and policy; of a semver policy its range.
    • An imagerepository: interval; of an authored secretRef, proxySecretRef or certSecretRef its name; of an authored accessFrom its namespaceSelectors. image is 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 whose image is authored empty (image: required (…)).
    • An imageupdateautomation: sourceRef with its kind and name, and interval. Of an authored git, commit with its author and the author’s email; of an authored git.checkout, its ref; of an authored git.commit.signingKey, its secretRef with its name. Of a match expression of policySelector, its key and its operator (policySelector.matchExpressions[0].operator: required (…)); its values and the operator’s value are the API server’s.
    • An artifactgenerator: sources and artifacts; of each source its alias, kind and name; of each artifact its name and copy; of each copy its from and to.
    • A resourcesetinputprovider: type; of an authored secretRef or certSecretRef its name; of each schedule its cron. Of a match expression of a selector, its key and its operator (selectors[0].matchExpressions[0].operator: required (…)); its values and 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_RequiredMatchMarkers derives each list from the +required markers 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. The key and operator of 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, artifacts and copy of an artifactgenerator), and one longer than the API allows;
    • every value rule of the API but the pattern of a duration: enumerations (eventSeverity, a source’s kind, digestReflectionPolicy, a policy’s order), lengths (a source’s name and namespace, summary), and that a source with matchLabels is named *, which the type documents and no marker states;
    • a policy that names none of semver, alphabetical and numerical, or more than one: the type calls it a union, and no marker holds it to one;
    • of a fluxcd-provider, the enumeration of type, the lengths of channel, username, address and proxy, and the pattern of proxy (an http or https URL); an address that 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’s kind), the lengths of a resource’s name and namespace, the pattern of an issuerURL (an http or https URL; 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), a validations list authored empty, two OIDC providers of one issuerURL, which the type keys the list by, and that a resource with matchLabels is named *, which the type documents and no marker states;
    • of an imagerepository, the enumeration of provider, the length of serviceAccountName, an exclusionList of more than 25 entries, an accessFrom whose namespaceSelectors is authored empty, and whether image is a repository name the controller accepts: one that carries a tag or a digest is written as authored;
    • an imageupdateautomation with no git, which the type documents as “technically optional, but in practice mandatory” and no marker requires; the pattern of git.push.refspec and the enumerations (sourceRef.kind, update.strategy, a signing key’s type);
    • of an artifactgenerator, the patterns (a source’s alias, name and namespace, pathPattern, an artifact’s revision and originRevision, a copy’s from and to), the lengths and the enumerations (a source’s kind, a copy’s strategy); that an alias is unique and that a path, a revision or an originRevision names an alias that is declared, which the type documents and no marker states;
    • of a resourcesetinputprovider, the enumeration of type, the pattern of url (an http, https or oci URL) and the maximum of filter.limit (10000); the url scheme 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 resourcesetinputprovider kind 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_ExpressionRules reads every such rule from the markers of the linked source and holds the list (fluxRulesLeft) to them, and TestFluxOperatorKinds_ExpressionRulesMatchCRD holds the ResourceSetInputProvider’s list to the x-kubernetes-validations of 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: commitStatusExpr set on a type other than github, gitlab, gitea, bitbucketserver, bitbucket and azuredevops. It builds and is refused at apply.
    • A fluxcd-receiver: a generic-oidc receiver without an OIDC provider or with a secretRef, and a receiver of another type with oidcProviders or without a secretRef. Each builds and is refused at apply.
    • An imagepolicy: interval without digestReflectionPolicy: Always, and digestReflectionPolicy: Always without interval. Each builds and is refused at apply.
    • An artifactgenerator: where no pathPattern is set, every artifact’s name must be a Kubernetes object name (lower case letters, digits, - and .). One that is not, App_Manifests for one, builds and is refused at apply.
    • A resourcesetinputprovider: seventeen rules, sixteen of them on the spec and one on a selector. url must be left out under Static and ExternalArtifact and written under every other type, with the scheme the type takes (http or https for a Git, AzureDevOps and ExternalService provider, https for AWSCodeCommit, oci with a repository after the registry for an OCI provider); https for an ExternalService provider unless insecure is true, and insecure only under ExternalService and OCIArtifactTag; serviceAccountName only under the AzureDevOps, AWSCodeCommit, artifact-tag and ExternalArtifact types; secretRef and certSecretRef not under the types that take none; selectors written under ExternalArtifact and only there; and a selector’s name not beside matchLabels or matchExpressions. Each builds and is refused at apply.

    Policy. Two dimensions of the environment policy reach three of these objects: the allowed registries hold the image of an imagerepository and the host of a resourcesetinputprovider’s url, and a policy that forbids explicit secrets refuses a fluxcd-provider address that 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. eventMetadata is 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 address or proxy is 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. An address or a proxy that is no URL with a host is refused when it holds an @ (an address with no @ passes, since the API allows a project ID or a namespace there); bot:pw@host and bot@host parse as URLs with no user, so parsing alone does not clear them.
    • An address that 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, googlechat and lark post to the address with no token of their own, so it is a webhook URL whose path or query holds the token; slack does the same where its Secret holds no token, and the kind does not read the Secret, so a slack address is refused even at the public API endpoint a token-holding Provider names; generic and generic-hmac post to the address as written, and nothing tells an endpoint from one whose path or query is the credential. An azureeventhub address that holds SharedAccessKey is 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: omit address, set secretRef, and put the URL under the address key of that Secret, which the controller reads in place of address for every type (internal/server/event_handlers.go). An empty address passes, and so does every address under a policy that allows explicit secrets, one that does not answer the question, and none. TestProviderAddressCredentialTypes_InUpstreamEnum holds the list to the API’s values of type. 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 of address and proxy: 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 of secretRef, proxySecretRef and certSecretRef are Secrets the object names, not values.
    • No field of a Receiver holds an image or a secret. The token is in the Secret secretRef names. A user or a password in an OIDC provider’s issuerURL is refused, under every policy and under none, by the same rule as a Provider’s proxy (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’s issuerURL is 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 image is held to the allowed registries, by the rule that holds the image of a pod: under a policy that allows registry.example, image: ghcr.io/shop/web is refused (image "ghcr.io/shop/web" is not from an allowed registry [registry.example], a refusal of the class registry). A name with no registry is one of docker.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 Secret proxySecretRef names; insecure, which no dimension of the policy speaks of; provider and serviceAccountName. 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.messageTemplateValues and git.push.options are 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. commonMetadata holds 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 url is 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 Secret secretRef names. A url that is no URL with a host is refused too when it holds an @, as a Provider’s proxy is. The host of url is held to the allowed registries, whatever the type: the answer of that host decides what a ResourceSet deploys, the ExternalService call sends the referenced credential to it, and one rule holds a type a later operator version adds. An oci:// url is held by the rule that holds an ocirepository’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 allows registry.example, url: https://git.other.example/shop/fleet is refused, a refusal of the class registry. A Static or ExternalArtifact provider has no url and nothing is held. A policy that lists no registry refuses none, and neither does a build without a policy. Not held: serviceAccountName, insecure and the selectors of an ExternalArtifact provider, which name no host; the inputs the provider returns, which the operator reads at run time and no build sees. defaultValues is 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. FluxNamespaceReads reports 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 of secretRef, proxySecretRef and certSecretRef, a Receiver reads the Secret of secretRef, a ResourceSetInputProvider reads the Secrets of secretRef and certSecretRef, and an ImageUpdateAutomation reads the Secret of git.commit.signingKey.secretRef. The ServiceAccount of a Provider’s, an ImageRepository’s or a ResourceSetInputProvider’s serviceAccountName is not reported: the report holds ConfigMaps and Secrets only.

    Labels and annotations are the labels and annotations properties.

    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 url of a ResourceSetInputProvider answers, whether a commitStatusExpr or a Receiver’s CEL expression compiles, whether a filterTags pattern, a semver range, a commit message template, a pathPattern, a ResourceSetInputProvider’s filter expression or a schedule’s cron parses, 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 unless objectName names 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: postgresql emits 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-imagecatalog and cnpg-clusterimagecatalog (ImageCatalogSpec, which the two objects share): images, each an image, a major and its extensions (a name, an image volume source with its reference and pullPolicy, the four path lists and env), and componentImages, each a key and an image.
    • cnpg-backup (BackupSpec): cluster, target, method, pluginConfiguration (name, parameters), online and onlineConfiguration.
    • cnpg-scheduledbackup (ScheduledBackupSpec): the six of a Backup, and schedule, suspend, immediate and backupOwnerReference.
    • 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, replication and bypassrls.
    • cnpg-publication (PublicationSpec): cluster, name, dbname, parameters, target (allTables, or objects, each a tablesInSchema or a table with its name, schema, only and columns) and publicationReclaimPolicy.
    • cnpg-subscription (SubscriptionSpec): cluster, name, dbname, parameters, publicationName, publicationDBName, externalClusterName and subscriptionReclaimPolicy.
    • No default is filled. An unauthored method, ensure, inherit, backupOwnerReference or reclaim policy is left out and the API server fills its own.
    • A role’s connectionLimit: 0 is 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")): method on a Backup and a ScheduledBackup (barmanObjectStore), a ScheduledBackup’s backupOwnerReference (none), a role’s ensure (present) and databaseRoleReclaimPolicy (retain), and publicationReclaimPolicy and subscriptionReclaimPolicy (retain). The type omits an empty string, so the default would replace the authored value. With connectionLimit these are every such field of the seven specs; TestCnpgKindsDefaultedZeroFields_MatchCRD derives each kind’s list from the linked CRDs and fails on another.
    • A role’s validUntil is written in UTC. The API type holds an instant: 2030-01-01T02:00:00+02:00 is written 2030-01-01T00:00:00Z. It writes the instant to the second and the zero time as null, which the API server drops, so a validUntil with 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 is 0001-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’s date-time format 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’s image and major; an extension’s name, and the name and the value of each of its env entries; a component image’s key and image;
    • cluster, on the five kinds that refer to a Cluster, and its name on a cnpg-backup and a cnpg-scheduledbackup. The DatabaseRole, Publication and Subscription CRDs default the name to an empty string, which no Cluster has, so the other three refuse a reference with no name on launcher’s own account, with the same cluster.name: required;
    • a plugin configuration’s name, on a cnpg-backup and a cnpg-scheduledbackup that author one;
    • a cnpg-scheduledbackup’s schedule;
    • a cnpg-databaserole’s name, and the name of an authored passwordSecret;
    • a cnpg-publication’s name, dbname and target, and the name of a target’s table;
    • a cnpg-subscription’s name, dbname, publicationName and externalClusterName.

    TestCnpgFurtherKinds_RequiredMatchCRD holds 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: a cluster.name must be a name CloudNativePG admits for a Cluster (a DNS-1035 label of at most 50 characters), as on cnpg-pooler and cnpg-database; a role’s name: "" breaks a rule of its CRD (below); and a catalog’s image: "", 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 one key. The component images are a list keyed on key, so the API server refuses a key held twice by its schema as well as by the rule;
    • a role (eight): a name that is empty, postgres or streaming_replica, or that starts with pg_ or cnpg_ (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 with databaseRoleReclaimPolicy: delete); passwordSecret beside disablePassword: true; and a client certificate that is enabled without login: true. A certificate is enabled where its block is authored and enabled is not false: the API fills true into an omitted one before it evaluates the rule. It fills nothing into login, which the rule reads without asking whether it is there: an enabled certificate with no login authored is refused by the API server as a rule it cannot evaluate (no such key: login), and one beside login: false by the rule’s own message. The build gives both the one refusal;
    • a publication (three): a target that publishes neither or both of allTables: true and objects (target: one of allTables: true and objects is required); an object that is neither or both of tablesInSchema and table; and a table that lists its columns where another object publishes tablesInSchema. The Go type leaves out a false allTables and an empty objects, tablesInSchema or columns, so each is read as the object will hold it: allTables: false beside 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: false beside a list of objects is refused, and the object the build writes for it is accepted; allTables: false alone is accepted as authored and refused by the build, since the object written holds neither. TestCnpgPublication_TargetAsWritten shows the first for each of the four, and the second for the three that can stand alone (an empty columns alone 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; the cluster of a ScheduledBackup, a DatabaseRole, a Publication and a Subscription; the name of a role; the name and the dbname of a publication and of a subscription; and a publication’s target.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 on allTables sits 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 from allTables to objects, or back, is not refused by it. TestCnpgFurtherKinds_ExpressionRules lists 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. A cnpg-backup kept 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 (another objectName), 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 a cnpg-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 major below 10, and the forms of a key, of an extension’s name and of an env name;
    • 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 online and onlineConfiguration do not come with the barmanObjectStore method, which is also the method of a document that authors none, and, on a Backup only, that the plugin method comes with a pluginConfiguration;
    • parameters on 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’s pullPolicy). A validUntil is 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’s image, a component image’s image and the reference of 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-cluster and cnpg-pooler repeat 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_CnpgImageCatalogs holds the three, on both kinds.

    Policy. The two catalogs are held to the environment policy; the other five have no field an oam.Policy method 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. ApplyPolicy checks the three fields that name one, and refuses by the field’s path with the class oam.RefusalRegistry: an image’s image (images[0].image), a component image’s image (componentImages[0].image) and the reference of 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_ImagesAreHeldToTheAllowedRegistries holds the three, on both kinds.
    • A Cluster’s reference to a catalog is a name. A cnpg-cluster’s imageCatalogRef names 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 passwordSecret is 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’s parameters and an extension’s env are free text: a value written there is written into the object as authored and is not checked.

    Labels and annotations are the labels and annotations properties, 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/v1beta1 API: a BFDProfile. It is built on policyFreeKind as the kinds above are and emits that one object, named after the component unless objectName names 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, as spec: {}.

    • No default is filled, and every authored value is written. Each field is a pointer: an authored echoMode: false or passiveMode: false is 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_NoDefaultIsLost fails on a default a dependency bump adds to a field that could not carry an authored empty value.

    Required: nothing. TestMetalLBKinds_RequiredMatchCRD holds that to the linked module’s v1beta1 CRD.

    The CRD’s expression rules. The CRD declares none; TestMetalLBKinds_ExpressionRules fails 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 minimumTtl is for multi-hop sessions only.

    Labels and annotations are the labels and annotations properties. A BGPPeer refers to the profile by its name: name the component so, or set objectName.

    Policy. No field of the spec is one an oam.Policy method speaks to, so ApplyPolicy enforces 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.

  • metallb-bgpadvertisement (go-kure/launcher#790) is the kind-named projection of a third object of MetalLB’s metallb.io/v1beta1 API: a BGPAdvertisement. It is built on policyFreeKind as the two kinds above are and emits that one object, named after the component unless objectName names 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:

    • ipAddressPools names the pools and ipAddressPoolSelectors selects them by their labels. With no pool named or selected the advertisement applies to every pool MetalLB reads;
    • peers limits the BGP peers the addresses are announced to. With none, they are announced to every BGP peer MetalLB is configured with;
    • nodeSelectors limits the nodes that are announced as next hops for an address. With none, the advertisement excludes no node;
    • serviceSelectors limits the Services. With none, every Service that has an address of the selected pools is announced;
    • aggregationLength and aggregationLengthV6 roll the addresses up into a larger prefix, of that length, which is then what is announced in place of one route per address;
    • localPref and communities are 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_NoDefaultIsLost holds the defaults of the linked CRD to that.
    • localPref is a number the type omits at 0, with no default in the CRD: an authored localPref: 0 is 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 key and the operator of a match expression, in every selector of ipAddressPoolSelectors, nodeSelectors and serviceSelectors. The API requires nothing else of the spec. TestMetalLBKinds_RequiredMatchCRD holds the list to the linked module’s v1beta1 CRD.

    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 serviceSelectors beside an aggregationLength other than 32 or an aggregationLengthV6 other 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_ExpressionRules holds 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 aggregationLength below 1 is written as authored;
    • a community: no entry of communities is 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 labels and annotations properties. The pools ipAddressPools names are the object names of metallb-ipaddresspool components (the component’s name, or its objectName), and the labels ipAddressPoolSelectors queries are their labels. 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.Policy method speaks to, so ApplyPolicy enforces 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 the v1beta1 of the five kinds above: that is the version the linked module’s CRD stores, v1beta1 is served and marked deprecated there, and the base library’s constructor builds the peer at v1beta2. TestMetalLBKinds_EmitTheServedVersion holds each kind to the version its CRD serves and stores. The kind is built on policyHeldKind, the helper of the kinds above with one check under the environment policy, and emits that one object, named after the component unless objectName names 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:

    • myASN is the AS number of the cluster’s end, peerASN the one expected of the router, or dynamicASN (internal or external) where it is detected; localASN announces another AS number to this peer;
    • peerAddress is the address dialled, or interface the node’s interface of an unnumbered session; peerPort, sourceAddress, routerID and vrf are the rest of the connection;
    • holdTime, keepaliveTime and connectTime are the session’s timers;
    • password or passwordSecret authenticates the session (TCP MD5);
    • bfdProfile names 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 of dualStackAddressFamily) and dualStackAddressFamily are 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. disableMP and dualStackAddressFamily default to false, which is the value the type leaves out: an authored false is left out, and the API fills the same false back. peerPort defaults to 179 and the type leaves a 0 out, so an authored peerPort: 0 would 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_NoDefaultIsLost holds 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. For localASN that 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: 0s is written. A duration is written in the form the type gives it (90s as 1m30s).
    • myASN is 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 the key and the operator of a match expression in every selector of nodeSelectors. The API requires nothing else of the spec. TestMetalLBKinds_RequiredMatchCRD holds the list to the linked module’s v1beta2 CRD.

    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. A connectTime that 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.5s passes the first rule and breaks the second, and a part smaller than a millisecond is seen by neither, here or there. TestMetalLBKinds_ExpressionRules holds 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: enableGracefulRestart may 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 myASN other than 0; exactly one of peerASN and dynamicASN; exactly one of peerAddress and interface; not both password and a named passwordSecret. A component that breaks one of these builds;
    • the CRD’s other value rules: a peerPort above 16384 and a dynamicASN that is neither internal nor external are written as authored;
    • an address, a router ID and a VRF’s or an interface’s name: none is read;
    • the profile bfdProfile names and the Secret passwordSecret names: 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 labels and annotations properties. A BGPAdvertisement names the peers it announces to (peers): name the component so, or set objectName. The profile bfdProfile names is the object name of a metallb-bfdprofile component (the component’s name, or its objectName). nodeSelectors queries nodes, whose labels launcher does not write, and is the author’s.

    Policy. The environment policy reaches one field.

    • password is 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 a certificate is. 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 empty password is none. TestMetalLBBGPPeerKind_SessionPassword holds this.
    • passwordSecret names 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 type kubernetes.io/basic-auth, in the namespace of the MetalLB deployment, with the password under the key password; none of that is checked, and neither is the reference’s namespace.
    • 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.Policy method speaks to a field of the spec, and ApplyPolicy fills 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/v1beta1 API: a Community. It is built on policyFreeKind as the kinds above are and emits that one object, named after the component unless objectName names 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 name to one BGP community value, a standard one of the form 1234:1234 or a large one of the form large:1234:1234:1234. On its own it announces nothing. An item of a MetalLB BGPAdvertisement’s communities may 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, as spec: {}.

    • No default is filled. The CRD declares none. The name and the value of 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_NoDefaultIsLost fails on a default a dependency bump adds to a field that could not carry an authored empty value.

    Required: nothing. TestMetalLBKinds_RequiredMatchCRD holds that to the linked module’s v1beta1 CRD.

    The CRD’s expression rules. The CRD declares none; TestMetalLBKinds_ExpressionRules fails 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 labels and annotations properties. An advertisement refers to an alias by the alias’s name, not by the name of the object that holds it.

    Policy. No field of the spec is one an oam.Policy method speaks to, so ApplyPolicy enforces 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.

  • metallb-ipaddresspool (go-kure/launcher#790) is the kind-named projection of an object of MetalLB’s metallb.io/v1beta1 API: an IPAddressPool. It is built on policyFreeKind and emits that one object, named after the component unless objectName names it; the handler adds no label, no annotation and no default of its own. The type name carries the metallb- 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 serviceAllocation the pool is limited to no namespace and no Service: a Service of any namespace of the cluster may be given one of its addresses. serviceAllocation limits it to the namespaces it lists or selects and to the Services it selects. autoAssign: false keeps 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 --namespace flag or the METALLB_NAMESPACE variable, 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 its priority, namespaces, namespaceSelectors and serviceSelectors.
    • No default is filled, and an authored value that is the API’s default is not written where the Go type cannot hold it. autoAssign is a pointer: an authored false is written, and an unauthored one is left out, which the API fills with true. avoidBuggyIPs is no pointer and is omitted when false: an authored false is left out of the object, and the API fills the same false back. TestMetalLBKinds_NoDefaultIsLost holds 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’s priority is a number the type omits at 0, with no default in the CRD: an authored priority: 0 is 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 key and the operator of a match expression, in every selector of serviceAllocation.namespaceSelectors and serviceAllocation.serviceSelectors.

    An authored empty value satisfies the rule: addresses: [] is a pool with no address, which the CRD’s schema takes. TestMetalLBKinds_RequiredMatchCRD holds the list to the fields the linked module’s v1beta1 CRD 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_ExpressionRules fails 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 addresses is 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 labels and annotations properties. 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 set objectName. Its ipAddressPoolSelectors is a label query over pools: write the labels it names under the labels of the metallb-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 of serviceAllocation query 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.Policy method speaks to, so ApplyPolicy enforces 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/v1beta1 API: an L2Advertisement. It is built on policyFreeKind as metallb-ipaddresspool is and emits that one object, named after the component unless objectName names 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:

    • ipAddressPools names the pools and ipAddressPoolSelectors selects them by their labels. With no pool named or selected the advertisement applies to every pool MetalLB reads;
    • nodeSelectors limits the nodes that are announced as next hops for an address. With none, the advertisement excludes no node;
    • interfaces lists the interfaces of a node the address is announced from. With none, it is announced from all of them;
    • serviceSelectors limits 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 key and the operator of a match expression, in every selector of ipAddressPoolSelectors, nodeSelectors and serviceSelectors (nodeSelectors[1].matchExpressions[0].key: required (…)). The API requires nothing else of the spec. TestMetalLBKinds_RequiredMatchCRD holds the list to the linked module’s v1beta1 CRD.

    The CRD’s expression rules. The CRD declares none; TestMetalLBKinds_ExpressionRules fails 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 ipAddressPools is 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 labels and annotations properties. The pools ipAddressPools names are the object names of metallb-ipaddresspool components (the component’s name, or its objectName), and the labels ipAddressPoolSelectors queries are their labels. 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.Policy method speaks to, so ApplyPolicy enforces 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/v1 API. It is built on policyHeldKind (policyFreeKind with a policy check, see storageclass, below) and emits that one object in the build namespace, named after the component unless objectName names 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 alertmanager and config-reloader under containers, and init-config-reloader under initContainers (makeStatefulSetSpec in pkg/alertmanager/statefulset.go of prometheus-operator v0.94.1, the version the linked module is cut from); it generates no thanos-sidecar, so an entry of that name is a container of its own. Of the three, only alertmanager takes 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. additionalArgs is 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, tag and sha. Each is refused when not empty, beside an image too, 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, as kurel build does, 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’s name, a host alias’s ip and hostnames;
    • the server and the client of a clusterTLS;
    • the type of an updateStrategy;
    • under alertmanagerConfiguration.global: the host and the port of smtp.smartHost, and the clientId, clientSecret and tokenUrl of httpConfig.oauth2;
    • the key and operator of 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_RequiredMatchMarkers holds 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’s name, the key of 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 (the action of a container restart rule and the operator of its exit codes, the signerName and keyType of a pod certificate source), as on the pod kind.

    An authored 0 or "" the type cannot carry is refused. As on the pod kinds, on a listed container, init container or volume: a timeoutSeconds, periodSeconds, successThreshold or failureThreshold written as 0 on 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’s imagePullPolicy, a port’s protocol, an httpGet path or scheme, or a volume source’s default; and, under hostNetwork: true, a port’s hostPort written as 0, which the API server would set to the port’s containerPort. 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 empty portName, retention or alertmanagerConfigMatcherStrategy.type, which the CRD defaults to web, 120h and OnNamespace (portName: "" cannot be carried by the Prometheus operator API types (…)). An empty schedulerName or imagePullPolicy is 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_DefaultedZeros derives 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_HostNetworkHostPort shows the hostPort refusal. An authored hostNetwork: false is left out, and the API reads an absent one as false.

    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 retention of 1.5h or 1d, 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, at pkg/alertmanager/statefulset.go of prometheus-operator v0.94.1:

    • version unset where image, or a listed alertmanager entry, names the image: the operator chooses the container’s flags by version, 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 a version the operator fails the reconcile on: one semver.ParseTolerant cannot 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 version is below that version. An unset version (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 object name names 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: logFormat other than logfmt (0.16.0), web.getConcurrency and web.timeout (0.17.0), web.tlsConfig and web.httpConfig (0.22.0), clusterTLS (0.24.0), clusterLabel (0.26.0), enableFeatures (0.27.0), limits.maxSilences and limits.maxPerSilenceBytes (0.28.0), clusterPeerName (0.30.0). Under alertmanagerConfiguration.global: httpConfig.authorization, httpConfig.oauth2 and httpConfig.followRedirects (0.22.0); telegram (0.24.0); webex, httpConfig.enableHttp2, httpConfig.tlsConfig.minVersion and maxVersion, and httpConfig.oauth2.proxyUrl, noProxy and proxyConnectHeader (0.25.0); httpConfig.proxyFromEnvironment set to true, httpConfig.noProxy and httpConfig.proxyConnectHeader (0.26.0); jira, rocketChat and smtp.tlsConfig (0.28.0); smtp.forceImplicitTLS (0.31.0); mattermost (0.32.0). TestAlertmanagerVersionGates_MatchVendoredSource parses every version comparison of the operator’s package pkg/alertmanager (every Go file of it, its tests aside; at v0.94.1 the comparisons are in statefulset.go, amcfg.go and operator.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’s pkg/operator/defaults.go, are vendored unmodified under testdata/upstream/prometheus-operator, with the project’s LICENSE and NOTICE and a SOURCE file naming each file’s git blob id, and mise run vendor-prometheus-operator-gates <tag> re-fetches them for the tag go.mod links.
    • an enableFeatures Alertmanager exits on at startup, at version (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-108 at v0.34.0). So an element with a , in it is two, an empty element is refused (["classic-mode", ""]), and classic-mode with utf8-strict-mode is refused. A list of one empty element is refused at any version: the operator writes its empty value as --enable-feature alone (pkg/operator/argument.go:69-75 at v0.94.1), which Alertmanager’s flag parser refuses (kingpin v2.4.0 flags.go:126-130) before it exits; leave enableFeatures unset 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-metrics and utf8-strict-mode from 0.27; auto-gomaxprocs and auto-gomemlimit from 0.28; alert-names-in-metrics from 0.31; event-recorder and group-key-in-metrics from 0.33, which drops auto-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_MatchVendoredSource holds the names, and NewFlags statement by statement, to featurecontrol.go of each minor’s latest release, vendored unmodified under testdata/upstream/alertmanager with 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.34 re-fetches them.
    • a portName the API refuses where the operator writes it: not an IANA service name (at most 15 characters, lower-case letters, digits and -); unless listenLocal is set, mesh-tcp/mesh-udp, the container ports the operator adds beside the web port; and unless serviceName names 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.name beside storage.emptyDir or storage.ephemeral, other than alertmanager-<name>-db: the operator mounts the data volume under that name and creates it as alertmanager-<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 storage included) without spec.resources.requests.storage (unset or empty access modes, which are not serialized, the operator writes as ReadWriteOnce); 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 of 0 or 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 volumes named as a volume the operator adds: config-volume, tls-assets, config-out, web-config, cluster-tls-config, notification-templates where alertmanagerConfiguration.templates is set, the name the operator derives for each of secrets and configMaps (secret-<name>, configmap-<name>), and the data volume’s: beside storage.emptyDir or storage.ephemeral a 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-config and cluster-tls-config are refused whatever version names, though the operator adds them only for Alertmanager 0.22.0 and 0.24.0 on, so that raising version does 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 of secrets, or of configMaps, that the operator’s naming gives one volume name (alerts.config and alerts-config both name secret-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 volumeMounts at 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_out and certs, the web and cluster TLS configuration files whatever version names, /etc/alertmanager/templates where alertmanagerConfiguration.templates is set, and /etc/alertmanager/secrets/<name> and /etc/alertmanager/configmaps/<name> for each entry of secrets and configMaps (: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 additionalArgs whose name, or that name with no- 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 are config.file, storage.path, data.retention, web.listen-address, web.route-prefix, cluster.reconnect-timeout, cluster.listen-address (written cluster.listen-address= on one replica without forceEnableClusterMode), cluster.peer where a replica or additionalPeers is, and the flag of each field set among externalUrl, enableFeatures, web.getConcurrency, web.timeout, limits, logLevel other than info, logFormat other than logfmt, clusterAdvertiseAddress, the three cluster durations and clusterTLS (statefulset.go:289-508, :700-748), and cluster.peer-name, cluster.label and web.config.file. A flag the operator generates only from some version on is refused from that version on (cluster.peer-name 0.30.0, enable-feature 0.27.0, cluster.label 0.26.0, cluster.tls-config 0.24.0, web.config.file 0.22.0, web.get-concurrency and web.timeout 0.17.0, log.format 0.16.0, the limits flags 0.28.0), and always where version is 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. Not dispatch.start-delay: the operator leaves its own out where an argument names it.
    • an entry of alertmanagerConfiguration.templates whose key an earlier entry names, configMap or secret: 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.server or clusterTLS.client the 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: None without dnsConfig.nameservers, and a pod-level securityContext.windowsOptions.hostProcess: true without hostNetwork: 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 hostname alertmanager-<name>-<replicas-1> of the last pod must each be a DNS-1123 label; and the StatefulSet alertmanager-<name> must be at most 52 characters: the StatefulSet controller labels each of its pods controller-revision-hash with 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 where replicas is 0, with no pod to name or label. The refusal names the component, or objectName where that set the name.

    The API’s expression rules are not checked. The types the spec reaches state one: an updateStrategy with a rollingUpdate must have the type RollingUpdate. 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, and TestMonitoringWorkloadKinds_RulesListed fails 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: {} and alertmanagerConfigMatcherStrategy: {}; the API defaults the type of the second to OnNamespace. An authored storage carries a volumeClaimTemplate whether or not one was authored, with metadata: {} and status: {} beside its spec; upstream documents that an emptyDir or an ephemeral takes precedence over it.

    With or without a policy, an authored image (unless a patch of alertmanager names 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 the reference of 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 with resources as the operator fills it, an unset memory request as 200Mi whatever the limit, and with the requests and limits of a listed alertmanager entry 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 in resources. 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 is resources with containers[0] "alertmanager" merged over it), while a request the patch names replaces the 200Mi and is held with the limit of resources. 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 in containers or initContainers is 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.go at 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 under listenLocal), the merge takes the patch’s ports as listed. The operator’s ports, in its order, are the web port under portName at 9093/TCP and the config-reloader’s reloader-web at 8080/TCP unless listenLocal is set, mesh-tcp at 9094/TCP then mesh-udp at 9094/UDP, and the init-config-reloader’s reloader-init at 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 renames mesh-tcp, never mesh-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. Two volumes of one name are refused, as the API refuses the pods (volumes[1] "scratch": the name is listed already at volumes[0], …), and so is a serviceName that 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, clusterPushpullInterval and clusterPeerTimeout are 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 nonempty externalUrl that 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’s net/url cannot parse it, and, from v0.19.0 on, where its scheme is not http or https (externalUrl: not a URL of scheme http or https: …; cmd/alertmanager/main.go of v0.28.1, app/url.go of v0.34.0). The scheme is held where version is unset or names v0.19.0 or later, its prerelease v0.19.0-rc.0 included; an earlier version takes any scheme. Neither message names the value. A nonempty clusterAdvertiseAddress that is not an IP address and a numeric port written as host:port is refused where the cluster runs, at replicas other than 1 (0 included: the object configures the cluster for the pods a scale-up starts) or with forceEnableClusterMode (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 with net.SplitHostPort, reads the port with strconv.Atoi and 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-171 and app/app.go:214-234 of v0.34.0). The host is judged an IP address in full only where Alertmanager is built with Go 1.23 or later, whose net.ParseIP the check uses: from v0.28.0 on, its prerelease v0.28.0-rc.0 included (.promu.yml), and where version is 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, as 010.0.0.1 or fd00::00001), so before v0.28.0 only host: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 name alerts.example.com, is no IP address for the net.ParseIP of any Go and is refused at every version; one of those characters only, as cafe.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 as strconv.Atoi reads 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.go of Kubernetes v1.35.0). With the cluster off the value is not read. The message does not name the value.

    Policy. ApplyPolicy refuses or passes; it writes nothing, and without a policy the same component builds. Refused, each with the class a workload kind’s refusal has:

    • image where no patch of alertmanager replaces it, the image of a listed container or init container (a patch of alertmanager included), 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): the alertmanager container’s where neither image nor a patch of it names one, and config-reloader’s or init-config-reloader’s where no patch names one. See below;
    • replicas over 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 over resources. 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): of storage, the arm the operator uses, reading emptyDir, then ephemeral, then volumeClaimTemplate and 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 under volumes;
    • hostNetwork: true under 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 AllowedRegistries refuses 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: image or a patch of alertmanager for 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 replicas or 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_PodFieldsHeldOrListed derives, 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: secrets and configMaps mount the named Secrets and ConfigMaps of the namespace, read-only, into the alertmanager and config-reloader containers (makeStatefulSetSpec, same source), and fields such as retention and logLevel become 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. version does 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 serviceName is unset. None of it is in the object.
    • No policy default is filled. An unauthored replicas, resources or storage stays 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, under storage or volumes, 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, imagePullSecrets and volumeMounts.

    podMetadata is 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 into podMetadata.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 annotation kubectl.kubernetes.io/default-container. A value authored for app.kubernetes.io/version replaces the operator’s, which it copies podMetadata.labels over; 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.volumeClaimTemplate are 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). Beside storage.emptyDir or storage.ephemeral the operator makes no claim from it, and they are not read. Those of storage.ephemeral.volumeClaimTemplate and of a generic ephemeral volume under volumes go 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 web or clusterTLS key, an SMTP password, an OAuth2 client secret), and configSecret, secrets and imagePullSecrets name Secrets whose content is not in the object. TestMonitoringWorkloadKinds_CredentialsHeldOrListed derives 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 the env of 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 its alertmanager.yaml key, 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.

  • thanosruler (go-kure/launcher#790) is the kind-named projection of a ThanosRuler of the same API. It is built as alertmanager is, on policyHeldKind with the same policy check (monitoring_workload.go), and emits that one object in the build namespace, named after the component unless objectName names 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 of alertmanager above holds here unless this entry says otherwise: no capability is required and nothing gates the kind; an authored 0 the type cannot carry is refused on a listed container, and so is an empty portName, evaluationInterval, retention or action of a remoteWrite entry’s writeRelabelConfigs rule, which the CRD defaults to web, 15s, 24h and replace (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 where resources names 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); podMetadata is 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 of alertmanager has no counterpart: the operator passes a set duration through as written.

    One field is renamed: the spec’s labels is the externalLabels property. Every kind component’s labels property 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 a thanos_ruler_replica label with the pod’s name), not Kubernetes labels: the reserved-key and component-label checks of labels do not apply to them, and nothing is added to them. Authored together, labels lands on the object’s metadata and externalLabels on its spec.labels. externalLabels is decoded as strictly as the spec, under the same null contract, and an error names externalLabels, 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’s labels keeps its meaning. TestCoreKindSchemas_CoverSpec records it as a mapping (externalLabels for labels), 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, labels renamed as above: the image (image, version, imagePullPolicy, imagePullSecrets); the pods, as on alertmanager less hostNetwork, 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, additionalArgs and paused. additionalArgs is 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 of containers named thanos-ruler or config-reloader is a patch of the container the operator generates under that name (makeStatefulSetSpec, merged by k8s.MergePatchContainers), so such a patch may name no image; any other entry of containers 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 (thanos-ruler, config-reloader)). The operator generates no init container and adds each entry of initContainers as 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 with resources as the operator fills it, an unset memory request as 200Mi, and with the requests and limits of a listed thanos-ruler entry merged over that block key by key (makeStatefulSet and makeStatefulSetSpec, pkg/thanos/statefulset.go:62-67, :481 and :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 on alertmanager. 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 is resources with containers[0] "thanos-ruler" merged over it), while a request the patch names replaces the 200Mi and is held with the limit of resources. An image a thanos-ruler entry names replaces image, so image is 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’s name, a host alias’s ip and hostnames, the type of an updateStrategy; the namespace and resource of an excludedFromEnforcement entry, the ruleNamespace and ruleName of a prometheusRulesExcludedFromEnforce entry; and of a remoteWrite entry its url, the clientId, clientSecret and tokenUrl of its oauth2, and the clientId, tenantId (and, under azureAd.oauth, clientSecret) of its Azure AD settings; the key and operator of a match expression, in every label selector the spec holds, presence only, as on alertmanager. TestMonitoringKinds_RequiredMatchMarkers holds 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, at pkg/thanos/statefulset.go of prometheus-operator v0.94.1:

    • a portName the API refuses where the operator writes it: not an IANA service name; and grpc, the port the operator adds beside the web port on the thanos-ruler container unless listenLocal is set, and on the governing Service it creates unless serviceName names 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 writes portName nowhere else, as it gives the ruler no probe, so on the container it is judged as the pods run the web port: a thanos-ruler patch that names port 10902 otherwise leaves it out of the container, and one that renames grpc frees that name. The Service’s half is held whatever a patch says. A patch of thanos-ruler or config-reloader is merged by port number into the ports the operator gives them (grpc at 10901/TCP, the web port at 10902/TCP and reloader-web at 8080/TCP, the last two unless listenLocal is set), and a port name that leaves two of one name is refused, as on alertmanager. A serviceName must 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 beside emptyDir or ephemeral other than thanos-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 volumes named as a volume the operator adds: tls-assets, web-config (whatever version names, as on alertmanager), remote-write-config, the volume of each Secret key the spec sets (query-config, alertmanager-config, and objstorage-config, tracing-config and alertrelabel-config where 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 volumeMounts at 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: None without dnsConfig.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 volume thanos-ruler-<name>-rulefiles-0 must 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, or objectName where that set the name.

    Not checked of what the operator refuses: a ThanosRuler with neither queryConfig nor queryEndpoints (:141-143), a version it cannot parse (:145-149), an entry of additionalArgs naming a flag it generates (BuildArgs, :352), and a web.tlsConfig its 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 excludedFromEnforcement entry that leaves group out (or writes it null) is written with monitoring.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 authored monitoring.coreos.com is kept; another group is written as authored and left to the API server. TestMonitoringExcludedGroup_MatchesMarkers holds 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: an updateStrategy with a rollingUpdate must have the type RollingUpdate, and the externalId of a remote write entry’s sigv4 needs a roleArn.

    What the type writes unauthored: resources: {}, on every ThanosRuler; and, on an authored storage, a volumeClaimTemplate with metadata: {} and status: {}, as on alertmanager.

    Policy. Refused, with a workload kind’s classes: image, where no patch of thanos-ruler names one in its place, the image of a listed container or init container (a patch of thanos-ruler or config-reloader included) and an image volume’s reference outside the allowed registries; under a policy that lists allowed registries, the image of the thanos-ruler container where neither image nor a patch of it names one, and that of config-reloader where no patch names one; replicas over the replica maximum, an unset one counted as the 1 the StatefulSet runs; the cpu or memory of resources and of a listed container over the maxima, resources with a thanos-ruler entry’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 the storage arm the operator uses (emptyDir, then ephemeral, then volumeClaimTemplate, as on alertmanager) and of a generic ephemeral volume; a hostPath volume; a privileged listed container, a pod-level securityContext.windowsOptions.hostProcess and a capability the policy does not allow. The spec has no field for the host’s network, process or IPC namespace. TestMonitoringWorkloadKinds_PodFieldsHeldOrListed holds every field of the spec that shapes the pods to a refusal shown on a document or a stated reason, as for alertmanager.

    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 AllowedRegistries refuses 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: image or a patch of thanos-ruler for the first, a patch of config-reloader for the second. The operator generates config-reloader on 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 unset replicas into 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[].bearerToken holds 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, a keyFile). Not held: a credential under a name that does not say so. A value of remoteWrite[].headers, the userinfo of a URL (remoteWrite[].url, queryEndpoints, alertmanagersUrl), and free text (additionalArgs, the env of a listed container) are written as authored, under a policy that forbids explicit secrets too. TestMonitoringWorkloadKinds_CredentialsHeldOrListed derives the fields whose name suggests a credential and holds each to the policy or a stated reason.

    podMetadata. As on alertmanager: the pods carry the component label only if the author writes the component’s own value into podMetadata.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 annotation kubectl.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, the thanos-ruler-operated Service), is not in the object. The object’s status is the operator’s and is not written.

  • prometheus (go-kure/launcher#790) is the kind-named projection of a Prometheus of the same API. It is built as alertmanager is, on policyHeldKind with the same policy check (monitoring_workload.go), and emits that one object in the build namespace, named after the component unless objectName names 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 on alertmanager, nothing is held to version: an image named without one is not refused, nor is a version the operator cannot parse, and fields the operator reads only from some Prometheus version on are not held to version. What is said of alertmanager above 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 authored 0 or "" the type cannot carry is refused, a hostPort of 0 under hostNetwork: true included, and so is an empty schedulerName or imagePullPolicy, 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 empty portName, scrapeInterval, evaluationInterval or thanos.blockSize, which the CRD defaults to web, 30s, 30s and 2h, and an empty action of a relabeling rule (replace) in a remoteWrite entry’s writeRelabelConfigs, an Alertmanager endpoint’s relabelings or alertRelabelings, or a scrape class’s relabelings or metricRelabelings (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 of storage.volumeClaimTemplate are held to the reserved metadata keys where it is the arm in use, and no claim template takes a component label. An empty retention is not refused: its 24h default is the operator’s code (where retentionSize and retentionPercentage are 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 of alertmanager has no counterpart.

    No field is renamed. The spec has no top-level labels or annotations field, so every property keeps its upstream name: externalLabels is the spec’s own field, the Prometheus external labels, not Kubernetes labels, and the reserved-key and component-label checks of labels do 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 on alertmanager; 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, tag and sha are refused when not empty; an empty one writes nothing, as on alertmanager. The same three of thanos are refused whenever set, the empty string included, since the type writes a set pointer; a null one sets none (tag: not authorable: the Prometheus operator deprecates the field, …; use image). additionalArgs and thanos.additionalArgs are passed as written and are not read. An entry of containers named prometheus or config-reloader, or thanos-sidecar where thanos is set, is a patch of the container the operator generates under that name (makeStatefulSetSpec, merged by k8s.MergePatchContainers); of initContainers, 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 a thanos-sidecar entry without an image where thanos is unset, since no sidecar is generated then. As on alertmanager, 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, …); and dnsPolicy: None without dnsConfig.nameservers, and a pod-level securityContext.windowsOptions.hostProcess: true without hostNetwork: 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 on alertmanager: the prometheus container’s web port under portName at 9090/TCP and config-reloader’s reloader-web at 8080/TCP, none of either under listenLocal (config-reloader keeps its port under the ProcessSignal reloadStrategy, to which the operator does not hand listenLocal on), init-config-reloader’s reloader-init at 8081/TCP, and thanos-sidecar’s http at 10902 and grpc at 10901, which thanos.listenLocal does 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 two volumes of one name and a serviceName that is not a DNS-1035 label. A nonempty externalUrl that 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’s net/url cannot parse it (externalUrl: not a URL Go's net/url can parse: …; computeExternalURL, cmd/prometheus/main.go of 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 name of an additional argument (the spec’s and the sidecar’s), a DNS option’s name, a host alias’s ip and hostnames, the type of an updateStrategy; the namespace and resource of an excludedFromEnforcement entry, the ruleNamespace and ruleName of a prometheusRulesExcludedFromEnforce entry; alerting.alertmanagers, and an endpoint’s name and port; apiserverConfig.host; a scrape class’s name; shardRetentionPolicy.retain.retentionPeriod; tracingConfig.endpoint; the url of a remoteWrite and a remoteRead entry and the clientId, clientSecret and tokenUrl of its oauth2; the clientId, tenantId (and, under azureAd.oauth, clientSecret) of a remote write entry’s Azure AD settings; and the key and operator of a match expression, in every label selector the spec holds, presence only, as on alertmanager. TestMonitoringKinds_RequiredMatchMarkers holds the list to the markers of the linked module’s source.

    The group of an excluded object is written. An excludedFromEnforcement entry that leaves group out (or writes it null) is written with monitoring.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_MatchesMarkers holds 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 alertmanager and by the same shared checks, under any policy and none, at pkg/prometheus/common.go and pkg/prometheus/server/ of prometheus-operator v0.94.1:

    • a portName the API refuses where the operator writes it: not an IANA service name; and unless serviceName names a Service of the author’s, grpc where thanos is 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 a shards below 1, for which it runs 1 shard (shardsNumber).
    • the storage arms as on alertmanager: a claim template named otherwise than prometheus-<name>-db beside storage.emptyDir or storage.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 volumes named as a volume the operator adds: config, tls-assets, config-out and web-config (the last whatever version names, though the operator adds it only for Prometheus 2.24.0 on); log-file where scrapeFailureLogFile is a file name without a directory, or queryLogFile is one and scrapeFailureLogFile is unset; thanos-prometheus-http-client-file where thanos is set, whatever thanos.version names; any prometheus-<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-01 is not reserved); the name the operator derives for each of secrets and configMaps (secret-<name>, configmap-<name>); and the data volume’s. Two entries of secrets, or of configMaps, 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 on alertmanager: 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 volumeMounts at 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/prometheus where log-file is added, /etc/prometheus/rules/prometheus-<name>-rulefiles-<n>, and /etc/prometheus/secrets/<name> and /etc/prometheus/configmaps/<name> for each entry of secrets and configMaps. 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.volumeMounts at a path the operator mounts a volume at in the thanos-sidecar container: /prometheus where objectStorageConfig or objectStorageConfigFile is set, and /etc/thanos/config, which it mounts from Thanos 0.24.0, reserved whatever thanos.version names, as web-config is (createThanosContainer, server/statefulset.go:642-671 and :730-738); and any field of an entry but name and mountPath (readOnly, recursiveReadOnly, subPath, subPathExpr, mountPropagation, bindMountOptions), which the operator drops: it copies only those two into the sidecar’s mount (:642-647). TestThanosDroppedMountFields holds the list to every field of the type.
    • a queryLogFile named without a directory beside a scrapeFailureLogFile named with one: the operator writes the query log under /var/log/prometheus but adds the log-file volume 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, in volumeMounts or a patch of the prometheus container, is accepted; a mount there with readOnly set, 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 keeps readOnly if 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.resources or 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 ConfigMap prometheus-<name>-rulefiles-2, and the hostname of the last pod of the last shard, prometheus-<name>-<replicas-1> or prometheus-<name>-shard-<shards-1>-<replicas-1>, must each be a DNS-1123 label; and the name of the last shard’s StatefulSet, prometheus-<name> or prometheus-<name>-shard-<shards-1>, must be at most 52 characters: the StatefulSet controller labels each of its pods controller-revision-hash with 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 where replicas is 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, or objectName where 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 mode Topology, shards must be at least the number of topology values, and topology needs that mode; an updateStrategy with a rollingUpdate must have the type RollingUpdate; and the externalId of a sigv4, a remote write entry’s or an Alertmanager endpoint’s, needs a roleArn.

    What the type writes unauthored: arbitraryFSAccessThroughSMs: {}, resources: {} and rules: {alert: {}}, on every Prometheus; on an authored thanos, its resources: {}; and, on an authored storage, a volumeClaimTemplate with metadata: {} and status: {}.

    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); replicas times shards over the replica maximum, as the operator counts them (below; replicas times shards 4 exceeds enforced maximum 3); the cpu or memory of resources, of thanos.resources and of a listed container over the maxima; the storage a claim requests over the storage maximum; a hostPath volume; hostNetwork where the policy does not allow the host network; a privileged listed container, a pod-level securityContext.windowsOptions.hostProcess and a capability the policy does not allow. TestMonitoringWorkloadKinds_PodFieldsHeldOrListed holds every top-level field of the spec that shapes the pods to a refusal shown on a document or a stated reason; shards and the sidecar’s fields are not top-level pod fields of the derivation, and shards, thanos.image and thanos.resources are held by TestPrometheus_ReplicasTimesShards and TestPrometheus_PolicyRefusals instead.

    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 AllowedRegistries refuses 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: image or a patch of prometheus; where thanos is set, thanos.image or a patch of thanos-sidecar; a patch of config-reloader; and a patch of init-config-reloader in initContainers. The operator generates the prometheus container, both reloaders and, where thanos is 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 replicas as 1 and an unset shards as 1 (ReplicasNumberPtr, shardsNumber; a shards below 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 bearerToken of a remoteWrite entry, of a remoteRead entry and of apiserverConfig holds 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 a headers map (remoteWrite, remoteRead, tracingConfig), the userinfo of a URL, and free text (additionalArgs, the env of a listed container) are written as authored, under a policy that forbids explicit secrets too. TestMonitoringWorkloadKinds_CredentialsHeldOrListed derives the fields whose name suggests a credential and holds each to the policy or a stated reason.

    podMetadata. As on alertmanager: the pods carry the component label only if the author writes the component’s own value into podMetadata.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 annotation kubectl.kubernetes.io/default-container. The operator copies the authored labels and annotations over app.kubernetes.io/version and 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 (the prometheus-operated Service, 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.

  • webservice / worker — image, replicas (default 1), port (webservice), plus the full DeploymentSpec-level surface they share with deployment — strategy, minReadySeconds, revisionHistoryLimit, paused and progressDeadlineSeconds (go-kure/launcher#341, see “Deployment-level properties” above). What still separates them from deployment is launcher’s own opinions, which these two keep: topologySpread and the four-key affinity shorthand, and on webservice the port that drives the Service. Note the shorthand is what they keep — neither publishes the raw corev1 affinity/topologySpreadConstraints that deployment does. The webservice rule implements the optional oam.EndpointProvider: it declares its own pods (app: <component-name>) on the declared port (its single port property drives both the container port and the Service port), letting a downstream platform synthesize generic app→app connections targeting a webservice. worker declares no in-cluster port and emits no Service, so it deliberately advertises no endpoint (not an EndpointProvider).

    • worker is a component lowering rule, not a handler (WorkerRule, go-kure/launcher#280). It runs worker’s own parse, then re-expresses the component as a deployment of the same name: the authored properties are forwarded, the four-key affinity shorthand is evaluated exactly as before and forwarded as the raw corev1 affinity (omitted when it evaluates to nothing), and topologySpread becomes a synthesized topology-spread trait placed before the authored traits (none when topologySpread: false). Authored traits and annotations are forwarded unchanged. Worker’s published schema, its generated output and the app: <component-name> selector are unchanged — every example and golden fixture builds byte-identically — and the emitted component carries Origin.Rule component/worker@v1alpha1. Two differences are deliberate. A worker refused by its own parse now reads component "w" (type "worker") in document …: <cause> (the lowering engine’s prefix) where it read component "w": <cause>; the cause is unchanged. A trait-lowering error on a worker’s trait gains one chain line naming the component/worker@v1alpha1 step; 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 raw affinity is forwarded, with the same affinity: 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, or podActiveDeadlineSeconds, which only Job pods may set. Any other key worker does not declare is dropped rather than forwarded to deployment; kurel build refuses both before lowering anyway.
    • Both rules emit the component’s ServiceAccount as a serviceaccount member (go-kure/launcher#702) unless serviceAccountName is authored. A worker therefore lowers to a same-name sibling group of a deployment and a serviceaccount; a webservice to a deployment, a service and a serviceaccount. The member carries automountServiceAccountToken: false, the component’s annotations, and the authored prune-protection and force-replace traits. The deployment member is handed the account’s object name as serviceAccountName (the component name, unless serviceAccountObjectName or the Naming hook 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 lowers webservice or worker must register a serviceaccount component handler too, or the build fails with member type "serviceaccount" has no component handler.
    • Both rules turn each claim a pvc volume describes into a synthesized pvc trait on the deployment member (go-kure/launcher#702), named <component>-<volume> as before unless the volume’s claimObjectName or the Naming hook names it (below), and rewrite the volume to reference it by claimName, keeping accessModes so the non-RWX constraints still see it. The claims are generated as before, after the component’s own objects. A registry that lowers webservice or worker must register the pvc trait handler too.
    • webservice is 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 (see pkg/oam “Same-name sibling groups”): a deployment and then a service, both named after the component, deployed as one component — one tier, one bundle. The deployment member gets the authored properties webservice declares except port, topologySpread and affinity, plus the main container’s one port {name: http, containerPort: <port>}; the affinity shorthand and topologySpread are handled as on worker. The service member gets one port {name: http, port: <port>} and the default selector app: <component-name>. Annotations go to both members. Each authored trait is forwarded unchanged, in authored order, to the member it acts on: expose, ingress and httproute go to the service; prune-protection and force-replace go to both; every other trait goes to the deployment. That includes fluxcd-patches and fluxcd-postbuild (not built in; a consumer that delivers through Flux registers them) and any trait type an extension registered, since the deployment member 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 carries Origin.Rule component/webservice@v1alpha1. Four differences are deliberate. A refusal from webservice’s own parse gains the lowering engine’s prefix, as on worker; the cause is unchanged. The synthesized inbound NetworkPolicy opens an ingress portName: http as the port’s number, where the handler opened the name http, which the container port also carried; the pods admitted are the same. Likewise, an ingress path on another component whose backend names a webservice’s Service by portName opens that port’s number on the webservice’s pods, translated as on a service component. A route on another component whose backend names a webservice’s Service on a port that Service does not expose (port: 9090 on a webservice whose port is 8080) no longer opens that port on the webservice’s pods: the service member opens only its own declared ports, as a service component 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 webservice component name must be a valid Service name. It always emits a Service named after the component, and the API server validates a Service’s metadata.name 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, so api.v1, a 64-character name and 1api are 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). Generate applies the same rule to the Application name it is handed. worker emits 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, serviceObjectName and serviceAccountObjectName name 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 (webservice only; worker generates 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 the Naming hook is asked (roles workload-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 one serviceObjectName, or a service component whose object carries a webservice’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 app label, 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 pvc volume’s claimObjectName names the claim that volume generates (role workload-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 the Naming hook is asked once per such volume. A volume with claimName references an existing claim and generates none, so claimObjectName beside 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 scaler trait’s scaleTargetRef names 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’ serviceAccountName and the rbac trait’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 env value, a URL in another component’s properties, a backend another component’s route names) is the author’s to change.
      • serviceAccountObjectName names the account the component generates, serviceAccountName an account that exists. With serviceAccountName the component generates none, so the two together are refused (serviceAccountObjectName and serviceAccountName are both set: …), and the hook is not asked for an account.
    • An ingress portName on a webservice must be http. The Service’s one port is named http, so an ingress path whose implicit backend is this component’s Service (no backend, or backend naming that Service) and that addresses it by any other portName is refused at build time (cannot route implicit backend to port "name"), just as a numbered port other than the component’s port is (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 with webservice and worker rather than owning. What distinguishes it is mostly what it leaves out: no topologySpread, no four-key affinity shorthand, no port; it declares no endpoint and emits no Service. It is not worker minus 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 raw corev1 scheduling surface the role kinds lack: affinity, tolerations and topologySpreadConstraints as 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. selector defaults to app: <component name>, the label every workload kind puts on its pods; set it when the Service fronts a workload named differently (a deployment named api-server behind a service named api). ports is the full corev1.ServicePort list, at least one entry unless the Service is headless or of type ExternalName: port (required), targetPort (a number or a container port name, published as the Types: [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 to port), protocol (TCP, UDP or SCTP; defaults to TCP), name (required once there is more than one port; names and port/protocol pairs must be unique), nodePort (1-65535, only with type NodePort or LoadBalancer, 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 an ExternalName Service, which the API accepts; the cluster’s own node port range is narrower and is checked on apply) and appProtocol (a qualified name such as http or kubernetes.io/h2c). type is ClusterIP (default), NodePort, LoadBalancer or ExternalName. An empty selector is refused.

    • ExternalName. type: ExternalName (go-kure/launcher#790) emits a DNS alias for externalName, which is then required: a DNS-1123 subdomain, with one trailing dot allowed. The Service selects no pods, so selector is refused with it (launcher’s own rule; the API accepts and ignores one), and so are clusterIP: None, ipFamilies and ipFamilyPolicy; ports is optional. On any other type externalName is 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: an ingress or httproute trait refuses it as an implicit backend (component "db" has no service port), a trait-level servicePort is 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’s backendSelector is 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 ServiceSpec fields (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.

      PropertyRules
      externalTrafficPolicyCluster or Local. Only with type NodePort or LoadBalancer.
      internalTrafficPolicyCluster or Local.
      trafficDistributionPreferSameZone, PreferSameNode, or PreferClose (the deprecated name of PreferSameZone).
      sessionAffinityNone or ClientIP.
      sessionAffinityConfigOnly with sessionAffinity: ClientIP. clientIP.timeoutSeconds is required once the key is authored (launcher’s own: the API server would default it), 1-86400.
      publishNotReadyAddressesA boolean; written only when true.
      ipFamiliesIPv4 and/or IPv6, each once, at most two.
      ipFamilyPolicySingleStack, PreferDualStack or RequireDualStack; SingleStack is refused with two ipFamilies (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: LoadBalancer and externalTrafficPolicy: Local. 0 is refused (launcher’s own: the API reads it as “allocate one”; leave the key out instead).

      externalIPs and clusterIPs are refused, and so is a literal clusterIP address. externalIPs routes 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: externalIPs and clusterIPs as unsupported fields, each with the kind’s reason after the list of allowed keys (go-kure/launcher#790), and a literal clusterIP address as a value outside the property’s enum, which has None alone.

    • Headless. clusterIP: None (go-kure/launcher#690) emits spec.clusterIP: None: no virtual IP, and cluster DNS resolves the name to the selected pods, as a StatefulSet’s governing Service needs. It requires type: ClusterIP (the default), since the API server refuses a headless NodePort or LoadBalancer Service. None is the only value: a literal address is refused, and so is an empty string, rather than read as absence. A headless Service may have no ports at all; without clusterIP, at least one port is still required (ports: at least one port is required), except on an ExternalName Service (above). A port-less Service has no first port, so routing traits refuse it as an implicit backend, refuse a trait-level servicePort on 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’s backendSelector is not trusted and, with no TCP port to translate, no allow is synthesized. Without clusterIP the Service carries no clusterIP, 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, so api.v1, a 64-character name and 1api are 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. Generate applies the same rule to the Application name it is handed.

    • Routing traits use the first port. ingress, httproute and expose (which lowers to one of the two) on a service component resolve their implicit backend to ports[0].port and refuse any other port on it, whether it is named by number (cannot route implicit backend to port N) or by an ingress path’s portName (cannot route implicit backend to port "name"); a portName must be the first port’s own name, so an unknown name is refused too. Naming the component’s own Service as the backend (backend: api on 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-traffic policy for traffic routed to a service selects its selector pods — not the component label, which no pod carries — and opens the targetPort of each routed TCP port. A route that reaches only a UDP or SCTP port synthesizes no policy, and neither does a route to an ExternalName Service, which has no pods to open a port on (above). In a same-name sibling group whose selector picks a sibling’s own pods, the policy selects the component label instead, but only when IdentityTargetPorts is true: the Service has at least one port and every port, whatever its protocol, has a numeric targetPort equal to its own port. Otherwise it keeps the selector. Either way the routed ports are translated to their targetPort and only TCP ones are opened (see pkg/oam “Same-name sibling groups”).

    • It implements oam.EndpointProvider: one endpoint, the selector pods on every TCP targetPort (deduplicated). A Service with no TCP port declares none, and neither does an ExternalName Service.

    • No policy check. service has no ApplyPolicy, 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’s type, so NodePort and LoadBalancer build under every policy. type: ExternalName has no capability gate either: whoever may author a service component 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, loadBalancerIP and externalTrafficPolicy are 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 one type.

  • 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. serviceaccount and configmap have no ApplyPolicy, deliberately (go-kure/launcher#794, item 2): none of their properties maps to an environment-policy method (imagePullSecrets names Secrets, not a registry), so there is nothing to enforce. persistentvolumeclaim has one, for the storage default and maximum.

    • serviceaccount publishes automountServiceAccountToken and imagePullSecrets ([{name}]). An unauthored automountServiceAccountToken stays unset, which Kubernetes reads as true. The account a role kind generates sets it to false, so a serviceaccount replacing that account authors false to keep the same posture. A workload runs as it through serviceAccountName, which also stops a role kind from generating its own account.

    • persistentvolumeclaim publishes size, storageClassName, accessModes (default [ReadWriteOnce]) and volumeMode. The claim is built by the same BuildPVC as a pvc volume, so the two agree on every field they share, including the explicit-empty storageClassName: "", which requests no class. The pvc trait is this kind’s twin (go-kure/launcher#741): it runs the kind’s own ParseClaimProperties, ApplyClaimPolicy and GenerateClaim, so the same properties build the same claim. Only the ownership fields differ: the claim’s name, its app label, namespace and bundle. An unauthored storageClassName (absent or null) comes from the ClusterProfile pvc capability’s storageClassName, 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 assigned storageClassName cannot 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 unauthored size comes from the EnvironmentPolicy storage default; with neither, the build fails with size: required …. The policy’s maximum storage size applies either way. A workload mounts the claim through a pvc volume’s claimName (see “Referencing an existing claim” below). A claim name another component also generates (for example a pvc trait 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 pvc trait alike (claim_spec.go). Each is written only when authored, so a document that authors none builds the claim it built before. A workload’s pvc volume does not take them.

      PropertyRule
      selector{matchLabels, matchExpressions[]}A label query over the PersistentVolumes that may back the claim, parsed as a statefulset claim 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 (the dataSourceRef row 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 ValidatePersistentVolumeClaimSpec does 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’s checkVolumeSatisfyClaim). The storage default and maximum still apply to the claim’s size.

      dataSource is refused: it is the superseded spelling of dataSourceRef, which the API server mirrors into it. The long resources spelling of size is not read on a standalone claim.

    • configmap publishes data (string values only: a number or a boolean is refused rather than stringified), binaryData (base64; a key may not also appear in data) and immutable. The data values and the decoded binaryData values 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 exported CheckConfigMapSize(data, binaryData). A key the API server refuses in a ConfigMap is refused by the exported ValidateConfigMapKey(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 the configmap trait, which attaches a ConfigMap to another component. The trait is this kind’s twin (go-kure/launcher#741): it reads data, binaryData and immutable through the exported ParseConfigMapProperties(props) and builds the ConfigMap through GenerateConfigMap(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 its objectName) and carrying the component’s app label, and nothing else, so a workload’s secret volume, envFrom or secretKeyRef names 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 in stringData, where the API server would let stringData win silently), type and immutable. An unauthored type and immutable stay unset (the API server stores Opaque for 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 under stringData: 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 (an external-secret trait, 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 exported ValidateSecretKey(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 an ApplyPolicy: a policy that forbids explicit secrets (oam.ExplicitSecretPolicy, see the pkg/oam README) 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 the secret trait, a helm component’s secretValues and a Secret a passthrough or manifests component carries. A policy that does not implement that interface, and no policy, allow it.
    • It is a different type from the secret trait, which attaches a Secret to another component. The trait is this kind’s twin: it reads the same four properties through the exported ParseSecretProperties(props) and builds the Secret through GenerateSecret(config, name, namespace, labels), and its schema is the kind’s plus name, 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. A secret component and a secret trait 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 a configmap component and trait do. The Secret a helm component generates for secretValues is claimed under role values-secret, as the same kind, so a secret component under that name is refused as a name collision with both named.
  • 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-database and cnpg-objectstore kinds: 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_CoverSpec holds 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 an app label 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, so ApplyPolicy is 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, and labels and annotations for the object’s metadata (“The object’s labels and annotations” below).

    • namespace publishes finalizers, the one field of corev1.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/enforce and its siblings) writes them under labels.
    • limitrange publishes limits, the one field of corev1.LimitRangeSpec, and emits the LimitRange in the build namespace. limits is required: the Go type cannot omit it, so an unauthored one (absent or null) would be written as limits: null and is refused with limits: required …. An authored limits: [] is kept: a LimitRange that enforces nothing. Each limit’s type is 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, min at most max, what a Pod limit may not set) are left to the API server. A quantity written as a number is emitted in its canonical string form (cpu: 2 becomes cpu: "2"). The limits constrain the pods and claims of the namespace at admission; the environment policy is not applied to them.
    • resourcequota publishes hard, scopes and scopeSelector, the fields of corev1.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: 10 becomes "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, limitrange and resourcequota: one schema key per json field of corev1.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 embeds corev1.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 requires capacity, accessModes and 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 the labels and annotations properties.

    Policy. Unlike namespace, limitrange and resourcequota, a PersistentVolume is held to the environment policy (enforcePersistentVolumePolicy):

    • A hostPath or local source is refused unless AllowHostPathVolumes() allows it. Both name a path on the node (local a disk, partition or directory there), and a pod that binds the volume through a claim reads and writes that path, as it does through a hostPath volume of its own.
    • capacity.storage is 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), passthrough and manifests run, 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 but csi and flexVolume name a remote endpoint or disk, not a path on the node, and are not gated. Not covered: a csi or flexVolume source. Each names a driver and options only the driver interprets, and no field says whether a node path is exposed; a pod’s csi and flexVolume volumes 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 passthrough component held or a manifests source yielded reached the output unchecked. One with a hostPath or local source, or with capacity.storage over the storage maximum, is now refused where it built before — and with no policy passed a hostPath or local one is always refused, since NoopPolicy allows no hostPath volume. A consumer allows it through its policy (AllowHostPathVolumes(), MaxStorageSize()), not per component.

  • 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 the app label and nothing else: no annotation, no ServiceAccount, no automountServiceAccountToken, no resources, no probe. The app label (app: <component>, see “The app label”) is the one every workload kind gives its pods and the one launcher’s traits and Services select on: a networkpolicy trait’s policy matches the Pod by it, and a service component reaches it with selector: {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 of webservice, worker or deployment — a property of those (image, ports, env at the top level) is refused as not a PodSpec field.

    Traits. The pod is a trait target: security-context, a configmap trait’s mountPath and an external-secret trait’s envFrom/mountPath change its spec as they change a workload kind’s pod template. topology-spread is Deployment-only and refuses a pod component.

    Refused when the component is read, with or without a policy:

    • ephemeralContainers, priority and overhead, 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 sets priorityClassName or runtimeClassName. The three are not in the schema, so a caller that validates the properties against it first, as kurel build does, 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 empty ephemeralContainers or overhead is 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 reference of 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’s timeoutSeconds, periodSeconds, successThreshold or failureThreshold (defaults 1, 10, 1, 3), on the liveness, readiness and startup probes of every init and regular container;
      • written as "": dnsPolicy (ClusterFirst), restartPolicy (Always) and schedulerName (default-scheduler); on every init and regular container imagePullPolicy, terminationMessagePath (/dev/termination-log), terminationMessagePolicy (File), a port’s protocol (TCP), an env entry’s valueFrom.fieldRef.apiVersion (v1), and the httpGet path (/) and scheme (HTTP) of its probes and of its postStart and preStop hooks; on a volume, the fieldRef.apiVersion of a downwardAPI item, also inside a projected source (v1), an image volume’s pullPolicy, iscsi.iscsiInterface (default), rbd.pool, rbd.user and rbd.keyring (rbd, admin, /etc/ceph/keyring) and scaleIO.storageMode and scaleIO.fsType (ThinProvisioned, xfs). An imagePullPolicy or image volume pullPolicy defaults to Always for an image tagged latest or naming neither a tag nor a digest, else to IfNotPresent; the image rule refuses the first two, so an accepted image gets IfNotPresent. The code defaults an image volume’s pullPolicy only under the ImageVolume feature gate, which is on and locked since Kubernetes 1.36;
      • written as 0 when hostNetwork is true: a container port’s hostPort, which the API server sets to the port’s containerPort. 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. serviceAccountName and its deprecated spelling serviceAccount are kept equal, so an authored "" with neither set stays ""; the default service account is set by an admission plugin, not by defaulting. A string field that is a pointer in the Go type (a gRPC probe’s service, an httpGet protocol, a container’s restartPolicy, a hostPath type, 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 linked k8s.io/api under its Apache-2.0 licence, in testdata/upstream/kubernetes/ (see the next paragraph). The test walks the generated SetObjectDefaults_ 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, a break out 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 a SetDefaults_ function. The exceptions are a port’s protocol and the iscsi, rbd and scaleIO strings, which k8s.io/api declares with a +default marker and the generated functions apply, and hostPort, which defaultHostNetworkPorts sets. 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’s LICENSE, the licence’s full text, at the same tag. Its SOURCE file names the tag and the git blob id of each file and of LICENSE; the tests check each against its file, the tag against the linked k8s.io/api, and fail when LICENSE is missing. mise run vendor-k8s-defaulters vX.Y.Z re-fetches it when that module moves. CI never runs it, and the tests need no network.

    • A match expression without its key or operator, with an operator that is none of the four, or with values that do not go with the operator, in every label selector of the spec: a pod affinity or anti-affinity term’s labelSelector and namespaceSelector, a topology spread constraint’s labelSelector, a projected clusterTrustBundle source’s labelSelector and a generic ephemeral volume’s claim selector (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 action of a container restart rule and the operator of its exit codes (restartPolicyRules[]), and the signerName and keyType of 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_OmittedRequiredAndWrittenDefaults lists the four with that reason.

    Policy. ApplyPolicy runs 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 its reference is 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:5000 is not registry.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 of replicaset, replicationcontroller, podtemplate and cnpg-pooler, and the pod spec of a workload that template delivery (helmtemplate), passthrough or a manifests source carries. The components with a volume schema of their own (webservice, worker, deployment, statefulset, daemonset, job, cronjob) take no image volume: their volumes has 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 linked k8s.io/api type and fails on a field whose name holds image, 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 that passthrough, a manifests source 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-pooler its pgbouncer.image and the images of its template, cnpg-cluster its imageName and the reference of each extension, postgresql the image it composes from version (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 :latest image 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), passthrough and manifests do not refuse priority or overhead on a Pod they emit; the admission controllers decide there. They do refuse ephemeral containers and an untagged or :latest image, an image volume’s included.

    The config implements oam.ServiceAccountNamer: the account is the authored serviceAccountName, or the deprecated serviceAccount where that one is unset, as the API server reads the two; with neither, the namespace’s default account. The Pod carries the app label and the component label, and what labels and annotations author 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 a deployment component 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 app label. The handler adds one thing: app: <component> on the pod template’s labels, beside the authored ones (see “The app label”). It is the label every workload kind gives its pods and the one launcher’s traits and Services select on, so a networkpolicy trait’s policy and a service component with selector: {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.app with 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 (app under DoesNotExist, or NotIn the label value) is refused (selector: rules out the label `app: web` …). A selector on app: <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 its key or operator, an unknown operator, values that 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 pod kind is: security-context, a configmap trait’s mountPath and an external-secret trait’s envFrom/mountPath change its pod template. topology-spread and scaler are 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 as null, and launcher derives none. Whether it matches the template’s labels, like the API’s other value rules (an empty selector, restartPolicy other than Always, a negative replicas), is left to the API server.
    • Under template.spec, what the pod kind 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 as 0, a defaulted string written as "", a hostPort of 0 under hostNetwork, a defective match expression in a label selector of the pod spec). A test holds the probe list to every non-pointer omitempty number or boolean under ReplicaSetSpec whose field comment states a non-zero default, the template’s metadata included, and TestKubernetesDefaulters_MatchVendoredSource holds 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; a pod or 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.spec either, for the reason the pod kind gives: no linked module shows that the API server refuses the omission.

    Policy. ApplyPolicy holds replicas to 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 the pod kind 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 unset replicas stays 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), passthrough and manifests do not refuse priority, overhead or activeDeadlineSeconds on a ReplicaSet they emit, do not require a selector and add no app label; the API server decides there. They do refuse ephemeral containers and an untagged or :latest image, an image volume’s included.

    The config implements oam.ServiceAccountNamer, reading the template’s serviceAccountName as the pod kind reads its own. The ReplicaSet’s own labels and annotations are the labels and annotations properties; the template’s metadata is carried as authored and takes neither.

  • replicationcontroller (go-kure/launcher#790) is the kind-named projection of a v1 ReplicationController, on the replicaset kind’s recipe: one schema key per json field of corev1.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 the replicaset kind states holds here — the app label on the pod template and the refusal of an authored one with another value, the three traits, the refusals under template.spec (activeDeadlineSeconds included), 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, and labels and annotations for the object’s own metadata — with these differences:

    • selector is a plain map of label to value, not a label selector: a matchLabels or matchExpressions key is refused as not a ReplicationControllerSpec field.
    • selector is optional. An unset one stays unset, and the API server defaults it to the pod template’s labels, the app label included.
    • template is 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 app label: 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 into corev1.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 own kind, apiVersion and metadata included (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 app label to the template (an authored app label is carried as written), the config reports no ServiceAccount (oam.ServiceAccountNamer is not implemented, so an rbac trait binds the component name), and it is not a trait target: a configmap trait’s mountPath, an external-secret trait’s envFrom/mountPath and a security-context trait’s pod-spec properties (runAsUser and the five others) are refused on it, naming the component and the kinds they apply to. security-context with psaLevel alone 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 the pod kind refuses of its own spec, by the same checks and named by path (template.spec.containers: required, which an unauthored template also gives; template.spec.priority: not authorable …; the image rule; a probe timing written as 0; a defaulted string written as ""; a hostPort of 0 under hostNetwork, 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.activeDeadlineSeconds is allowed, unlike on a replicaset or replicationcontroller: 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 the pod kind 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. ApplyPolicy holds the template to the check the pod kind 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), passthrough and manifests do not refuse priority or overhead on a PodTemplate they emit; the API server decides there. They do refuse ephemeral containers and an untagged or :latest image, an image volume’s included. The PodTemplate’s own labels and annotations are the labels and annotations properties; 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/v1 StorageClass, VolumeAttributesClass and CSIDriver, a scheduling.k8s.io/v1 PriorityClass, a node.k8s.io/v1 RuntimeClass and a networking.k8s.io/v1 IngressClass. Each emits that one object, named after the component unless objectName names 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, in kind_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, thanosruler and prometheus are built on policyHeldKind (kind_policy_held.go): this helper, unchanged, with an ApplyPolicy that 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 a null list 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 whose ApplyPolicy does nothing, and a Generate that 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 authored 0, false or "" on a listed field is refused, as the secret-store kinds do (see the secretstore entry) and alertmanager does 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_NoDefaultedZeros reads 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_DefaultedZeros reads the default markers of their source instead, strings included. Cilium’s publish none either, and its module ships the CRDs: TestCiliumBGPKinds_DefaultsSitOnPointers holds 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: a horizontalpodautoscaler’s metrics and its scaling rules’ policies, and a networkpolicy’s policyTypes. Two lists the API defaults are not refused, because an empty list already means their default: csidriver’s volumeLifecycleModes (an empty list means Persistent) and imagerepository’s exclusionList (the image-reflector controller filters tags through GetExclusionList, which returns the default list when the list is empty, so an API client’s [] means the default too). TestKindComponents_DefaultedEmptyLists walks 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 by TestKubernetesDefaulters_ListDefaultsMatchVendoredSource, from the excerpt the pod kind’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.

    • ingressclass and csidriver have a spec type, and the properties are its json fields: controller and parameters (IngressClassSpec), and the eleven fields of CSIDriverSpec (attachRequired, podInfoOnMount, volumeLifecycleModes, storageCapacity, fsGroupPolicy, tokenRequests, requiresRepublish, seLinuxMount, nodeAllocatableUpdatePeriodSeconds, serviceAccountTokenInSecrets, preventPodSchedulingIfMissing). A csidriver requires no property; an ingressclass requires controller (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, apiVersion and metadata are 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) and controller (ingressclass) must be a non-empty string (provisioner: required …), and a volumeattributesclass must carry at least one of parameters (parameters: required …). The upstream type marks controller optional; the API server’s validation requires it all the same. The API does not require a PriorityClass value: the type always encodes one, so a priorityclass that authors none is emitted with value: 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 false or 0 is 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: false and an empty description.

    The object’s name is the component’s unless objectName, or the Naming hook under role object, names it otherwise (see “The object name” below). Each handler declares its object as cluster-scoped, so the name is claimed in no namespace: two storageclass components 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’s storageClassName, a pod’s runtimeClassName or priorityClassName. The API identifies a CSI driver by the CSIDriver’s name, so a csidriver component’s object name must be the driver’s (csi.example.com), written as the component name or as objectName. 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. The csidriver kind 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.Policy method speaks to, so ApplyPolicy enforces 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 labels and annotations properties, 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 ingressclass whose parameters leaves scope out is emitted with scope: null: the API type always encodes the field, and documents Cluster as its default. Author scope to emit a value.
    • Whether a referenced object exists: the runtimeclass a pod names, the storageclass a claim names, the parameters object of an ingressclass.
  • 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 of networkingv1.IngressSpec (ingressClassName, defaultBackend, tls, rules), the property map decoded strictly into that type under the null contract, two spellings of one field refused, and TestCoreKindSchemas_CoverSpec holding 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 a defaultBackend nor a rule, a path without a pathType and 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 ingress trait, 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 expose or ingress trait on the backend component, or authors the NetworkPolicy (the networkpolicy trait 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. ApplyPolicy is 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 the ingress trait does not refuse an ingress component. passthrough, manifests and 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). An ingress trait names its own Ingress (<component>-ingress, or its name); a component and a trait whose Ingress would carry one name are refused, with both named.

    Labels and annotations are the labels and annotations properties, a controller’s nginx.ingress.kubernetes.io/… annotations among them. Not covered: its status, which the controller writes.

  • 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 of gatewayv1.HTTPRouteSpec, those of the CommonRouteSpec it inlines included (parentRefs, useDefaultGateways, hostnames, rules), decoded strictly into that type. No type under HTTPRouteSpec unmarshals 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’s type matching the member set, a rule’s matches and backendRefs limits) are left to the API server. A null list element (rules: [null], a null parent or backendRef) is refused by its path. useDefaultGateways is 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 httproute trait, 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, its namespace included. 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 the expose or httproute trait on the backend component, or authors the NetworkPolicy.
    • No parent from a capability. The trait builds parentRefs from the Gateway its capability rendering names when the author writes none. Here parentRefs is what the author wrote; unwritten, the route has no parent.
    • No environment policy. ApplyPolicy is a no-op, and the policy’s capability lists gate trait types, so a policy that forbids the httproute trait does not refuse an httproute component. passthrough, manifests and 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. An httproute trait names its own HTTPRoute (<component>-httproute, or its name); a component and a trait whose HTTPRoute would carry one name are refused, with both named.

    Labels and annotations are the labels and annotations properties. Not covered: its status, which the Gateway controller writes.

  • grpcroute (go-kure/launcher#790) is the kind-named projection of a gateway.networking.k8s.io/v1 GRPCRoute, on the httproute kind’s recipe and not on policyFreeKind as the TCP, UDP and TLS routes are: one schema key per json field of gatewayv1.GRPCRouteSpec (parentRefs, useDefaultGateways, hostnames, rules), decoded strictly into that type, and one GRPCRoute emitted in the build namespace, named after the component unless objectName names 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, their namespace included. As for httproute, no NetworkPolicy allow rule is synthesized for its backends, no parent comes from a capability, and ApplyPolicy is 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 labels and annotations properties. Not covered: its status, 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 of networkingv1.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 of podSelector, or of a peer’s podSelector or namespaceSelector, is refused without its key or operator, with an operator that is none of the four, or with values that do not go with the operator (see “A label selector’s match expressions”); the config is exported, so Generate repeats what the typed spec can show. The API’s other value rules (a peer naming at least one of podSelector, namespaceSelector and ipBlock, an ipBlock beside 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 networkpolicy trait 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:

    • podSelector is not defaulted. The trait has no such property: it always selects its component’s pods. Here an unwritten podSelector, 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.
    • policyTypes is not derived. The trait has no such property either: it lists a direction when that direction’s key is present, so its egress: [] denies all egress. Here policyTypes is what the author wrote; unwritten, the API server derives it: Ingress always, Egress only when egress holds a rule. So on the kind egress: [] alone isolates no egress; a policy that denies all egress writes policyTypes: [Egress]. An authored policyTypes: [] 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 empty ingress or egress list is carried as an unwritten one, which the API reads the same way.
    • A selector picks pods by label. A peer’s podSelector is not a component reference and nothing resolves it; the label a workload component’s pods carry is described under “The app label” below.
    • No environment policy. ApplyPolicy is a no-op, and the policy’s capability lists gate trait types, so a policy that forbids the networkpolicy trait does not refuse a networkpolicy component. passthrough, manifests and 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-traffic and the others) are additive beside it. The name is the component’s, or its objectName; a component whose policy would carry the name of a networkpolicy trait’s (<component>-allow, or its name) or of a synthesized one is refused, with both named. The NetworkPolicy’s own labels and annotations are the labels and annotations properties.

  • 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 under specs, or both, each a Cilium api.Rule. The component’s properties are those two fields, decoded strictly into that type under the null contract, and TestCoreKindSchemas_CoverSpec holds the two keys to the linked CiliumNetworkPolicy type. 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 spec nor an entry in specs. An empty specs holds none; beside a spec it 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, egress and egressDeny. 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 operator was emitted as operator: "", an authentication: {} as mode: "", a terminatingTLS: {} as secret: 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 key and operator of a match expression, in every selector of a rule (endpointSelector, fromEndpoints, toEndpoints, fromNodes, toNodes, a CIDR entry’s cidrGroupSelector, a k8sServiceSelector’s selector), under the deny lists too;
    • a k8sServiceSelector’s selector, the key of a rule label and the type of an icmps field;
    • under an allow list only: an authentication’s mode; a port’s listener name, its envoyConfig and that configuration’s name; the secret of a port’s terminatingTLS and originatingTLS and that Secret’s name; an HTTP header match’s name, and the name of its secret where one is authored.

    Two more are optional to the API and required here: a listener’s priority and the kind of its envoyConfig. The type writes an unauthored one as 0 and as the empty string, and the API refuses both (a priority is 1 to 100, a kind is CiliumEnvoyConfig or CiliumClusterwideEnvoyConfig), so a listener that leaves one out cannot be emitted. TestCiliumNetworkPolicy_RequiredMatchCRD holds 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, under spec and under specs, so a dependency bump that adds, drops or moves one fails there. The CRD names a nodeSelector’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 its key is refused earlier, by Cilium’s own decoding, as an icmps field without its type is (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 internal pkg/oam/internal/requiredfields (CiliumRule), since the cilium-networkpolicy trait 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’s cidrGroupSelector, under the deny lists too) and the two other types under a rule that unmarshal themselves: an icmps field (family, type) and an entry of labels in 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 icmps field needs its type. Cilium’s own decoding of the field panics when type is 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 an icmps field without a type.

    It is an authored object, not the cilium-networkpolicy trait, although the two share the type name. The trait attaches to a component and publishes one rule’s name, endpointSelector, ingress and egress; this kind is a component of its own, named after the component or by its objectName, and publishes the whole rule and specs. A component and a trait whose policy would carry one name (the trait’s name) are refused, with both named. Neither fills a selector. No environment policy applies: ApplyPolicy is a no-op, and the policy’s capability lists gate trait types, so a policy that forbids the cilium-networkpolicy trait does not refuse a cilium-networkpolicy component. passthrough, manifests and 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’s value is written into the object as authored and is not refused, since a header value cannot be told from a credential; a header match’s secret refers to a Secret by name and keeps the value out of the document. The object’s own labels and annotations are the top-level labels and annotations properties; a labels under spec or a specs entry is the rule’s own field. Not covered: its status, which the Cilium agent writes.

  • servicecidr (go-kure/launcher#790) is the kind-named projection of a cluster-scoped networking.k8s.io/v1 ServiceCIDR: a range the API server assigns Service cluster IPs from, beside the one it was started with. It is built on policyFreeKind, as the six kinds of the storageclass entry are, and what that entry says of them holds here: one object, named after the component unless objectName names 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 labels and annotations properties.
    • 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.
  • poddisruptionbudget (go-kure/launcher#790) is the kind-named projection of a policy/v1 PodDisruptionBudget, built on policyFreeKind. It emits that one object in the build namespace, named after the component unless objectName names 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: minAvailable and maxUnavailable (each a count or a percentage string such as "50%"), selector (a label selector) and unhealthyPodEvictionPolicy. A count is emitted as a number and a percentage as a string, and an authored 0 is kept (maxUnavailable: 0 allows 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 its key or operator, with an operator that is none of the four, or with values that 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 that minAvailable and maxUnavailable exclude 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 selector selects no pod, and selector: {} selects every pod of the namespace. To cover a workload component, select its app label (matchLabels: {app: <component>}), which holds the component name up to 63 characters and its label projection beyond.
    • Beside the scaler trait. scaler with enablePDB: true derives 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 writes minAvailable: 50%; here the count or percentage is the author’s, as are maxUnavailable, an eviction policy and pods launcher does not own).
    • Policy. No field of the spec is one an oam.Policy method speaks to, so ApplyPolicy enforces nothing and fills nothing.
    • Labels and annotations are the labels and annotations properties.
    • Not covered. Whether the selector matches any pod. The object’s status is the disruption controller’s and is not written.
  • endpointslice (go-kure/launcher#790) is the kind-named projection of a discovery.k8s.io/v1 EndpointSlice, built on policyFreeKind. It emits that one object in the build namespace, named after the component unless objectName names 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, endpoints and ports. The object’s kind, apiVersion and metadata are launcher’s and are refused under any spelling. An authored false is kept (conditions: {ready: false}), and an endpoint’s deprecatedTopology is 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-name label, authored under labels. Launcher derives the label from nothing and points it at no component: it does not follow a service component’s objectName, so write the name the Service object takes. A slice without the label builds and belongs to no Service. A service component of a type other than ExternalName always carries a selector (the kind refuses an empty one, and an ExternalName Service has no endpoints), so a Service whose endpoints are all written by hand comes from elsewhere: a manifests component, 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’s addresses (endpoints[0].addresses: required …), and the name of an entry of an endpoint’s hints.forZones or hints.forNodes. TestBuiltinMarkerKinds_RequiredMatchMarkers derives the list from the +required and +optional markers of the linked k8s.io/api module 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_OmittedRequiredAndWrittenDefaults holds 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 endpoints or no ports carries endpoints: null or ports: null, an authored empty list is written as [], and an endpoint that authors no condition carries conditions: {}. A port that authors no protocol or no name is written without it, and the API server fills TCP and 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.Policy method speaks to, so ApplyPolicy enforces nothing and fills nothing.
    • Labels and annotations are the labels and annotations properties.
    • 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 documents addressType as 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.
  • role, rolebinding, clusterrole, clusterrolebinding (go-kure/launcher#790) are the kind-named projections of the four objects of the rbac.authorization.k8s.io/v1 API, built on policyFreeKind. Each emits that one object, named after the component unless objectName names 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 the rbac trait (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, apiVersion and metadata are launcher’s and are refused under any spelling.
    • Presence rules read by hand. Beyond the +required markers of the linked k8s.io/api module, 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.go of k8s.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_RequiredMatchMarkers derives 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_OmittedRequiredAndWrittenDefaults holds 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 a role or a clusterrole) must write its verbs (rules[0].verbs: required …, from the markers). By hand: it must name at least one of apiGroups and at least one of resources (rules[0].apiGroups: required …; "" is the core group), so a rule that lost its resources and grants nothing is refused. A clusterrole rule may instead name nonResourceURLs, and then names no apiGroups, resources or resourceNames (rules[0].nonResourceURLs: not allowed beside …). These two hand-read checks count what was decoded, so an authored empty apiGroups: [] or resources: [] is refused too.
    • A binding (rolebinding, clusterrolebinding) must write roleRef with its kind and name, and each subject’s kind and name (roleRef: required …, subjects[0].name: required …, from the markers). subjects may be left out: the API server’s validation requires none, and the key is then not written.
    • roleRef.apiGroup may be left out. The object then carries apiGroup: "", 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 its apiGroup out 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.name and a subject’s name and namespace are written as authored. Launcher points none of them at a component: a roleRef.name does not follow the objectName of a role or clusterrole component, and a ServiceAccount subject’s name does not follow a serviceaccount component’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.Policy method speaks to, so ApplyPolicy enforces nothing and fills nothing.
    • Labels and annotations are the labels and annotations properties.
    • Beside the rbac trait. 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 roleRef names 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 the values of a match expression of an aggregation selector fit its operator. 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 the roleRef of 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 whole rbac.authorization.k8s.io group, out of a build, and a policy that does not allow cluster-scoped objects refuses the ClusterRole and the ClusterRoleBinding. It holds the rbac trait’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.
  • 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. nonResourceURLs are 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 carries rules: null and 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-admin included, builds. Its properties are subjects and roleRef, which names a Role of the binding’s namespace or a ClusterRole. A ServiceAccount subject may leave its namespace out: 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 rules and aggregationRule. By hand: an authored aggregationRule must hold at least one entry of clusterRoleSelectors (aggregationRule.clusterRoleSelectors: required …); an empty selector ({}) is an entry. From the markers, a selector’s match expression must write its key and operator. By hand again, an expression that writes both is refused for an operator that is none of the four and for values that 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 with rules: null when rules is not authored and with rules: [] 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-admin to any subject builds. Its properties are subjects and roleRef, which names a ClusterRole. By hand: a ServiceAccount subject must write its namespace (subjects[0].namespace: required …), since the binding is in none.

  • horizontalpodautoscaler (go-kure/launcher#790) is the kind-named projection of an autoscaling/v2 HorizontalPodAutoscaler. It emits that one object in the build namespace, named after the component unless objectName names 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, metrics and behavior. 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 authored 0 is kept (stabilizationWindowSeconds: 0). The type has no number or boolean that is omitted when zero, which TestHorizontalPodAutoscalerSpec_NoOmittedZeros holds it to.
    • An empty metrics or policies list is refused. The type omits metrics and a scaling rule’s policies when empty, and the API server then applies its default: 80% average CPU utilization for metrics, and its default policies for behavior.scaleUp and behavior.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 …) and maxReplicas (maxReplicas: required …; an authored 0 is refused the same way, since the type cannot tell it from an unset one). The config is exported, so Generate repeats both. Every other value rule is left to the API server: a negative maxReplicas, the kind and name inside scaleTargetRef, minReplicas against maxReplicas, which metric source goes with which type. The label selector of a metric (metrics[].object.metric.selector, and under pods and external) 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. maxReplicas is held to the environment policy’s replica maximum (MaxReplicas()), as the scaler trait holds its own and as a HorizontalPodAutoscaler a chart renders, a passthrough component holds or a manifests source yields is held: component "web": maxReplicas: replicas 4 exceeds enforced maximum 3. Nothing else is held, and no default is filled: the policy’s scalerMinReplicas and scalerMaxReplicas defaults belong to the scaler trait. A nil policy checks nothing.
    • The target is the author’s, and is not checked. scaleTargetRef names an object by kind, name and apiVersion; 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 its objectName. The guards the scaler trait applies to its own workload do not see this kind: a deployment, webservice or worker with a claim that is not ReadWriteMany refuses a scaler trait whose maxReplicas is above 1, and builds beside a horizontalpodautoscaler component that scales it further. That is the state of an autoscaler emitted through passthrough, manifests or template delivery.
    • Beside the scaler trait. 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>-hpa for the trait), and two that are given one name in one namespace are refused as a name collision.
    • Labels and annotations are the labels and annotations properties.
    • Not covered. Whether the target exists or can be scaled. The object’s status is the controller’s and is not written.
  • 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/v1 API: a ServiceMonitor, a PodMonitor, a Probe and a PrometheusRule. Each is built on policyFreeKind and emits that one object in the build namespace, named after the component unless objectName names 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 its name, rules and evaluation settings. A rule’s expr is 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, nativeHistogramMinBucketFactor and convertClassicHistogramsToNHCB.
    • An authored 0 or false is kept where the API tells it from an unset field (sampleLimit: 0, filterRunning: false, a group’s limit: 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.1 as 1100m). The API’s two defaults are strings (a prober’s path, /probe, and a relabeling rule’s action, 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_DefaultedZeros holds 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’s endpoints and selector, and a podmonitor’s selector. No default selector is filled: unauthored, the type would write selector: {}, which selects every Service or pod of the selected namespaces. selector: {} and endpoints: [] are authored values and build.
    • the clientId, clientSecret and tokenUrl of an oauth2, on an endpoint of either monitor and on a Probe;
    • a rule group’s name and a rule’s expr (unauthored, the type would write expr: 0);
    • the key and operator of a match expression, in a monitor’s selector and in a Probe’s targets.ingress.selector (selector.matchExpressions[0].operator: required (…)). The selector is a Kubernetes type, which carries no +required marker: 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 its values are 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 name of a Probe parameter (params[1].name: required (…)), unauthored or empty. The object would show the omission and the API server refuse it. TestKindComponents_OmittedRequiredAndWrittenDefaults derives 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 httproute kind is not in it: the experimental-channel CRD requires the protocol of an externalAuth filter, 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.url here, 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 require prober, but the type writes one whether or not it was authored, and the API server refuses a prober without a url. A Probe without a prober cannot be emitted. An empty url is refused as an unauthored one is.

    TestMonitoringKinds_RequiredMatchMarkers holds 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 key of 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.url and a parameter’s name excepted. 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 servicemonitor or podmonitor carries namespaceSelector: {}, which selects the object’s own namespace. A podmonitor without endpoints carries podMetricsEndpoints: null, which the API server drops as it drops every null of a field that is not nullable. A prometheus-probe carries targets: {}, and the operator rejects a Probe with no target. A tlsConfig carries ca: {} and cert: {} 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 app label (matchLabels: {app: <component>}, valued as “The app label” below describes); to scrape a service component’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 what labels authors: 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.Policy method speaks to, so ApplyPolicy enforces 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, a proxyUrl (of an endpoint, a prober or an oauth2), an oauth2’s tokenUrl, 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, an oauth2’s clientSecret, a tlsConfig’s keySecret, the values of proxyConnectHeader. Free text that could hold one is written to the object as authored, under a policy that forbids explicit secrets too: params, an oauth2’s endpointParams, and a proxyUrl with credentials in it.
    • File paths are not checked. A servicemonitor endpoint may name a file in the Prometheus container (bearerTokenFile, and caFile, certFile and keyFile under tlsConfig).

    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/v1 API: an Issuer, a ClusterIssuer and a Certificate. Each is built on policyHeldKind (policyFreeKind with a policy check, see the storageclass entry) and emits that one object, named after the component unless objectName names it; the handler adds no label, no annotation and no default. An issuer and a certificate are emitted in the build namespace and declare their object as namespaced. A clusterissuer is 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). The certificate trait 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).

    • issuer and clusterissuer (IssuerSpec): the five issuer types, acme, ca, vault, selfSigned and venafi. 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), with revisionHistoryLimit.
    • A duration is carried in Go’s spelling. duration, renewBefore and 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: 2160h is emitted as 2160h0m0s.
    • An authored false or 0 is kept where the API tells it from an unset field (encodeUsagesInRequest: false, a keystore’s create: false, a solver pod’s runAsUser: 0), and left out where the type omits a zero that means the same (isCA: false). TestCertManagerKinds_NoDefaultedZeros holds 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 authored 0 and 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, and issuerRef with its name. No default issuer is filled. Of what is authored below them: an additional output format’s type, and a keystore’s create.
    • An issuer has no required top-level field. Of an issuer type that is authored: acme.server and acme.privateKeySecretRef; ca.secretName; vault.server, vault.path and vault.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-DNS host, an RFC 2136 nameserver, a webhook solver’s groupName and solverName).
    • Of a reference to a Secret or a ServiceAccount that is authored, on either kind: its name. Unauthored, the type would write name: "".
    • Of a renewal window of a certificate that is authored (renewal.windows[]): its cron and its windowDuration (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 empty cron is refused as an unauthored one is. TestKindComponents_OmittedRequiredAndWrittenDefaults derives 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 nodeSelectorTerms of a required node affinity of the solver pod, under http01.ingress and under http01.gatewayHTTPRoute (acme.solvers[0].http01.ingress.podTemplate.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms: required (…)), and the name of a parent reference (acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].name: required (…)). Unauthored, the type writes the terms as null, 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_NullRequired shows 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 under gatewayHTTPRoute), on an issuer and a clusterissuer: its key and operator. Presence only: the operator’s value and its values are 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_RequiredMatchCRD holds 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 the key and operator of an expression at each of the 16 selectors), and nothing else is. A dependency bump that adds, drops or moves one fails there. TestLabelSelectorKinds_CoverEverySelector shows, at each of those selectors, what the API server answers for an expression without its key or its operator. Not refused:

    • every other required field of a Kubernetes or Gateway API type these specs embed (the key and operator of a node selector requirement, the topologyKey of 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 venafi issuer names exactly one of tpp, cloud and ngts);
    • 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 password beside a passwordSecretRef that names a Secret, or with neither (the empty reference the type writes beside an authored password is not one); a certificate that names no subject and no alternative name; subject or commonName beside literalSubject. Launcher repeats none of them, so an issuer with 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’s passwordSecretRef (so a keystore that authors password carries both fields), vault.auth.kubernetes.secretRef, an RFC 2136 solver’s tsigSecretSecretRef and a Route 53 solver’s secretAccessKeySecretRef. An HTTP01 solver’s podTemplate carries metadata: {} and spec: {}, and its ingressTemplate metadata: {}. 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.resources sizes 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 the pod kind’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-level securityContext, serviceAccountName, priorityClassName and imagePullSecrets are the author’s and are held to nothing, as are its nodeSelector, affinity and tolerations, 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.password and keystores.pkcs12.password hold 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, a venafi platform’s url and token endpoint, an RFC 2136 nameserver, an ACME-DNS host, an Akamai serviceConsumerDomain, 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 labels and annotations properties. A solver’s podTemplate.metadata is 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’s ingressTemplate.metadata and the labels of its HTTPRoutes reach an Ingress and HTTPRoutes, and are not read.

    The issuer of a certificate is the author’s. issuerRef names an issuer by name, kind and group; launcher points it at no component and does not look for the issuer in the document. To have an issuer or clusterissuer component sign a certificate component, name its object: the component name, or its objectName. cert-manager reads an issuerRef without a kind as an Issuer of the Certificate’s namespace. The Secret secretName names 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 certificate trait. The trait derives a Certificate for the workload it is attached to, from a few properties and the issuer the cluster’s certificate capability names, and names it after its secretName. 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/v2 API: a CiliumBGPAdvertisement, a CiliumBGPClusterConfig, a CiliumBGPNodeConfigOverride and a CiliumBGPPeerConfig. Each is built on policyFreeKind and emits that one object, named after the component unless objectName names 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 the cilium- 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 its advertisementType (PodCIDR, CiliumPodIPPool, Service or Interface), service (addresses, aggregationLengthIPv4, aggregationLengthIPv6), interface (name), selector and attributes (communities, localPreference).
    • cilium-bgpclusterconfig (CiliumBGPClusterConfigSpec): nodeSelector and bgpInstances, each with its name, localASN, localPort and peers; a peer has a name, a peerAddress or an autoDiscovery, a peerASN and a peerConfigRef.
    • cilium-bgpnodeconfigoverride (CiliumBGPNodeConfigOverrideSpec): bgpInstances, each with its name, routerID, localPort, localASN and peers; a peer has a name, a localAddress and a localPort.
    • cilium-bgppeerconfig (CiliumBGPPeerConfigSpec): transport (peerPort, sourceInterface), timers (connectRetryTimeSeconds, holdTimeSeconds, keepAliveTimeSeconds), authSecretRef, gracefulRestart (enabled, restartTimeSeconds), ebgpMultihop and families, each with its afi, its safi and the advertisements it sends. A peer configuration that authors nothing builds, and carries nothing.
    • An authored 0 or false is 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), and gracefulRestart.enabled is always written. No default is filled. The API’s defaults (a peer’s peerASN, the three timers, transport.peerPort, ebgpMultihop, gracefulRestart.restartTimeSeconds) are the API server’s to fill where the field was not authored; TestCiliumBGPKinds_DefaultsSitOnPointers holds 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’s advertisements and an entry’s advertisementType;
    • a cilium-bgpclusterconfig’s bgpInstances, an instance’s name, a peer’s name, an autoDiscovery’s mode, the addressFamily of its defaultGateway, and the name of a peerConfigRef;
    • a cilium-bgpnodeconfigoverride’s bgpInstances, an instance’s name and a peer’s name;
    • a cilium-bgppeerconfig family’s afi and safi, and the enabled of a gracefulRestart, which has no default: unauthored, the type would write enabled: false;
    • the key and the operator of a match expression, in every selector of the four: nodeSelector, an advertisement’s selector, a family’s advertisements. 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_RequiredMatchCRD holds these lists to the fields the linked module’s v2 CRD 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 name of an interface (advertisements[0].interface.name: required (…)) and the addresses of a service, an authored addresses: [] 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, and TestKindComponents_OmittedRequiredAndWrittenDefaults shows 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 advertisementType against the presence of one sibling: service is required with Service and refused with another type, interface is required with Interface and refused with another type, and selector is refused with PodCIDR (advertisements[1].service: not allowed with advertisementType "CiliumPodIPPool", only with "Service"). An authored {} is a present field and an authored null an absent one, as the API server reads them;
    • a peer configuration’s timers.keepAliveTimeSeconds no larger than its timers.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: 100 alone is refused (timers.keepAliveTimeSeconds: 100 is larger than the hold time the API fills where timers.holdTimeSeconds is not set (90)), and so is holdTimeSeconds: 20 alone (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’s name: ""). 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 and ebgpMultihop, 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 labels and annotations properties. A peer configuration selects advertisements by label. A family’s advertisements is a label query over CiliumBGPAdvertisement objects: write the labels it names under the labels of the cilium-bgpadvertisement. The object also carries the component label, whose value is the component’s (see “Component label and ownership” in the OAM model). nodeSelector and an advertisement’s selector query 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.Policy method speaks to, so ApplyPolicy enforces 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 peerAddress in a cilium-bgpclusterconfig, and a peer’s localAddress and an instance’s routerID in a cilium-bgpnodeconfigoverride.
    • No field holds a literal secret, and none is checked. authSecretRef is 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 v2alpha1 version of these objects: the kinds emit v2. 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. A cilium-bgpclusterconfig’s and a cilium-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/v2 API: a CiliumCIDRGroup, a CiliumLoadBalancerIPPool, a CiliumEgressGatewayPolicy, a CiliumLocalRedirectPolicy and a CiliumNodeConfig. Each is built on policyFreeKind and emits that one object, named after the component unless objectName names 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 (Yes or No), blocks, each a cidr or a start with an optional stop, and disabled.
    • cilium-egressgatewaypolicy (CiliumEgressGatewayPolicySpec): selectors, each with a namespaceSelector, a podSelector and a nodeSelector; destinationCIDRs; excludedCIDRs; egressGateway and egressGateways, a gateway being a nodeSelector, an interface and an egressIP.
    • cilium-localredirectpolicy (CiliumLocalRedirectPolicySpec): redirectFrontend, an addressMatcher (ip, toPorts) or a serviceMatcher (serviceName, namespace, toPorts); redirectBackend (localEndpointSelector, toPorts); skipRedirectFromBackend; description. A port is a port, a protocol and a name.
    • cilium-nodeconfig (CiliumNodeConfigSpec): defaults, a map of configuration keys to string values, and nodeSelector.
    • 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’s skipRedirectFromBackend and an egress gateway policy’s egressGateways. An authored disabled: false, skipRedirectFromBackend: false or egressGateways: [] 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_NoDefaultIsLost holds 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’s externalCIDRs;
    • a cilium-egressgatewaypolicy’s selectors, destinationCIDRs and egressGateway, and the nodeSelector of egressGateway and of every entry of egressGateways;
    • a cilium-localredirectpolicy’s redirectFrontend and redirectBackend; an addressMatcher’s ip and toPorts; a serviceMatcher’s serviceName and namespace; the backend’s localEndpointSelector and toPorts; the port and the protocol of every port, in each of the three lists;
    • a cilium-nodeconfig’s defaults and nodeSelector;
    • the key and the operator of a match expression, in every selector of the five. A cilium-loadbalancerippool requires nothing else.

    An authored empty value satisfies the rule: externalCIDRs: [] is a group that selects no peer, nodeSelector: {} every node and defaults: {} no key. TestCiliumPlainKinds_RequiredMatchCRD holds these lists to the fields the linked module’s v2 CRD 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 redirectFrontend is a choice between addressMatcher and serviceMatcher, 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_RedirectFrontendTakesOneMatcher holds 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_ExpressionRules lists each with why, and fails on one that is added or reworded.

    Not checked, and the API server’s to refuse:

    • a gateway’s egressIP being an IP address, the CRD’s rule on egressGateway.egressIP and on an egressGateways entry’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, redirectBackend and skipRedirectFromBackend once 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’s defaults: no key and no value is read. A key Cilium does not know builds;
    • an authored empty value in a required field (selectors: [], a serviceName: ""). It is a value;
    • every other value rule of the API: the enumerations (allowFirstLastIPs, a port’s protocol), the forms of a CIDR, an address, a port and a port name, and the number of egressGateways (at most 64);
    • what Cilium itself decides of an object the API server has admitted: a serviceMatcher whose namespace is 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 labels and annotations properties. A CIDR group is referred to by its name, or selected by its labels. A Cilium network policy’s cidrGroupRef names the group: name the component so, or set objectName. Its cidrGroupSelector is a label query over groups: write the labels it names under the labels of the cilium-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.Policy method speaks to, so ApplyPolicy enforces 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’s blocks, an egress policy’s destinationCIDRs, excludedCIDRs and egressIP, and a redirect frontend’s ip.
    • No field holds a literal secret or refers to a Secret, and none is checked. A cilium-nodeconfig’s defaults is free text: a value written there is written into the object as authored.

    Not covered. The served v2alpha1 version of the CIDR group, of the pool and of the node configuration: the kinds emit v2. 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 under specs, or both, each a Cilium api.Rule. The component’s properties are those two fields, decoded as the cilium-networkpolicy kind decodes them: strictly, under the null contract, with an unknown key inside a selector, an icmps field or a rule label refused by its path (see that entry). TestCoreKindSchemas_CoverSpec holds the two keys to the linked CiliumClusterwideNetworkPolicy type. It emits one object, named after the component unless objectName names it, with no namespace, declared cluster-scoped, holding the authored rules and, as on every kind component, the labels and annotations written under labels and annotations. It differs from the namespaced kind in two things: its rules reach every namespace of the cluster, and a rule selects endpoints or nodes, by endpointSelector or by nodeSelector.

    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 spec nor an entry in specs (the object’s expression rule has(self.spec) || has(self.specs)). An empty specs is not emitted, so it holds none; beside a spec it is accepted. TestCiliumClusterwideNetworkPolicy_ObjectRule holds the refusal to the API server’s own expression validator, which accepts an authored specs: [] and would refuse the object written without it;
    • a rule that authors both endpointSelector and nodeSelector, or neither: the schema takes exactly one. None is filled in. endpointSelector: {} selects every endpoint of the cluster and nodeSelector: {} 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, egress and egressDeny. 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 specs 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. Each is refused where its parent is authored, by its path (spec.ingress[0].toPorts[0].terminatingTLS.secret: required (…)):

    • the key and operator of a match expression, in every selector of a rule (endpointSelector, nodeSelector, fromEndpoints, toEndpoints, fromNodes, toNodes, a CIDR entry’s cidrGroupSelector, a k8sServiceSelector’s selector), under the deny lists too;
    • a k8sServiceSelector’s selector, the key of a rule label and the type of an icmps field;
    • under an allow list only: an authentication’s mode; a port’s listener name, its envoyConfig and that configuration’s name; the secret of a port’s terminatingTLS and originatingTLS and that Secret’s name; an HTTP header match’s name, and the name of its secret where one is authored.

    Two more are optional to the API and required here: a listener’s priority and the kind of its envoyConfig. The type writes an unauthored one as 0 and as the empty string, and the API refuses both (a priority is 1 to 100, a kind is CiliumEnvoyConfig or CiliumClusterwideEnvoyConfig), so a listener that leaves one out cannot be emitted. TestCiliumClusterwideNetworkPolicy_RequiredMatchCRD holds the whole list to the CRD of the linked module and to the rule type, under spec and under specs, so a dependency bump that adds, drops or moves one fails there. An icmps field without its type and a rule label without its key are refused earlier, by Cilium’s own decoding, with the decode error (for the type, 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 of cidr, cidrGroupRef and cidrGroupSelector; a DNS entry (toFQDNs, a port’s rules.dns) exactly one of matchName and matchPattern; a port’s rules exactly one of http and dns. TestCiliumClusterwideNetworkPolicy_SchemaChoices lists 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 authentication mode, 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-networkpolicy kind 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 icmps field’s family is left out when it is not authored, and the API fills IPv4, the one default the CRD declares in a rule. An authored family: "" is left out as well and reads the same, to Cilium too. A rule label in its object form without a source carries source: "", which the type writes and the API admits.

    Policy. ApplyPolicy is a no-op and no capability is required, as for the cilium-networkpolicy kind: 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 toFQDNs and of a port’s DNS rules, an HTTP rule’s host, a port’s serverNames, 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 value is written into the object as authored. A header match’s secret and a TLS context’s secret refer 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 its status, which the Cilium agent writes.

  • 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 on policyFreeKind (see the storageclass entry) and emits that one object, named after the component unless objectName names it; the handler adds no label, no annotation and no default. A gatewayclass is 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). The httproute trait may take the Gateway it attaches to from a capability (gatewayName, gatewayNamespace); the gateway kind does not read it, and a gateway component 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, parametersRef and description.
    • gateway (GatewaySpec): gatewayClassName, listeners, addresses, infrastructure, allowedListeners, tls and defaultScope. defaultScope is 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, and TestGatewayKinds_RequiredMatchCRD holds that.
    • listenerset (ListenerSetSpec): parentRef and listeners, whose entries take the fields of a Gateway’s listener.
    • referencegrant (ReferenceGrantSpec): from and to.
    • backendtlspolicy (BackendTLSPolicySpec): targetRefs, validation and options.
    • 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’s tls.mode (Terminate), an address’s type (IPAddress), the group and kind of a certificate reference (a core Secret) and of a parent reference (a Gateway), the group of an allowed route kind (the Gateway API’s), allowedListeners.namespaces.from (None), a client certificate validation’s mode (AllowValidOnly).
    • One number of these types is omitted when zero, a ListenerSet listener’s port, and the CRD gives it no default: an authored port: 0 is 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: the mode of a Gateway’s frontend TLS validation (AllowValidOnly), so an authored mode: "" would be left out and defaulted, and it is refused (tls.frontend.default.validation.mode: "" cannot be carried by the Gateway API types (…)). TestGatewayKinds_DefaultedZeros holds 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: gatewayClassName and listeners, and of each listener its name, port and protocol. Of a Gateway-wide tls.frontend that is authored: its default (default: {} asks for no client certificate), and of a perPort entry its port and tls; of a validation that is authored, its caCertificateRefs.
    • A listenerset: parentRef with its name.
    • A referencegrant: from and to; of a source its group, kind and namespace, of a target its group and kind. The core group is authored as group: "".
    • A backendtlspolicy: validation with its hostname; of a subject alternative name that is authored, its type.
    • Of a reference that is authored, on every kind, what the type would write empty: the name of a certificate reference (a listener’s tls.certificateRefs, the Gateway’s tls.backend.clientCertificateRef); the group, kind and name of a parameters reference (parametersRef, infrastructure.parametersRef), of a CA certificate reference (caCertificateRefs) and of a policy target (targetRefs); the kind of an allowed route kind (allowedRoutes.kinds).
    • Of a match expression that is authored in a namespace selector, on a gateway and a listenerset (a listener’s allowedRoutes.namespaces.selector, a Gateway’s allowedListeners.namespaces.selector): its key and operator, which the CRDs of both channels require. Presence only: the operator’s value and its values are 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, and TestLabelSelectorKinds_CoverEverySelector shows 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 listenerset with no listeners and a backendtlspolicy with no targetRefs, absent or empty (listeners: required (…)), as a servicecidr with no cidrs is;
    • a ListenerSet listener without its name, its port or its protocol, absent or empty, a port: 0 included (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_RequiredMatchCRD derives 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_RefusedOmissions holds each refused field to being required in both channels.

    TestGatewayKinds_RequiredMatchCRD holds 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 the key and operator of 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 a gateway, 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 tls and hostname against its protocol, the uniqueness of a listener’s name and of its port, protocol and hostname, of an address and of a perPort port, one of caCertificateRefs and wellKnownCACertificates, a subject alternative name’s field against its type, a target’s sectionName among several targets, a GatewayClass’s controllerName not 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.labels and infrastructure.annotations are 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.options and a BackendTLSPolicy’s options, 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’s value, a BackendTLSPolicy’s validation.hostname and the hostname and uri of 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 its objectName. A ListenerSet attaches only where its Gateway’s allowedListeners lets its namespace, and a reference across namespaces works only where a ReferenceGrant of the target’s namespace allows it; launcher checks neither. A referencegrant allows references into the namespace it is emitted in, so it belongs to the application of the objects referred to.

    Labels and annotations are the labels and annotations properties. A Gateway’s infrastructure.labels and infrastructure.annotations are 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/v1 that carry no HTTP: a TCPRoute, a UDPRoute and a TLSRoute. Each is built on policyFreeKind (above), as the kinds of the API’s infrastructure objects are, and emits that one object in the build namespace, named after the component unless objectName names it; the handler adds no label, no annotation and no default. The object is written at v1, the version the CRDs of both channels store; the older versions (v1alpha2, and v1alpha3 of 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 CommonRouteSpec it inlines included, decoded strictly at every depth: an unknown key is refused wherever it sits (a parent, a rule, a backend).

    • tcproute (TCPRouteSpec) and udproute (UDPRouteSpec): parentRefs, useDefaultGateways and rules. The API accepts exactly one rule.
    • tlsroute (TLSRouteSpec): the same three and hostnames, 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.
    • useDefaultGateways is 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, and TestGatewayKinds_RequiredMatchCRD holds that.
    • No default is filled. The defaults are the CRDs’, and the API server applies them to what the object leaves out: the group and kind of a parent (a Gateway), and the group, kind and weight of a backend (a core Service, weight 1).

    Required, by the rule of the infrastructure kinds above, is the name of 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 tlsroute with no hostnames (hostnames: required (…)).

    The httproute kind 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 no group and no kind is 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_RequiredMatchCRD holds each required list (2 paths) and the refused omissions to the CRDs the linked module ships, as it does for the infrastructure kinds. TestGatewayRouteKinds_OmissionsAreTheAPIServers and TestGatewayRouteKinds_ExpressionRules go 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 (by sectionName, and in the experimental channel by port too). 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 httproute is. 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 its objectName.

    • References across namespaces are not gated. parentRefs[].namespace and backendRefs[].namespace are written as authored, on the terms of the infrastructure kinds above and as the httproute kind 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 httproute above: 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. parentRefs is 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 hostnames are 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 labels and annotations properties. 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’s status, 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/v1 API: a SecretStore, a ClusterSecretStore, an ExternalSecret and a ClusterExternalSecret. Each emits that one object, named after the component unless objectName names it; the handler adds no label, no annotation and no default. A secretstore and an externalsecret are built in the build namespace. A clustersecretstore and a clusterexternalsecret are 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 on policyHeldKind: 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-secret capability 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 data entry, a template).

    • secretstore and clustersecretstore (SecretStoreSpec, which the two objects share): provider, with one provider under its key (aws, vault, kubernetes, webhook and the others the API lists), controller, retrySettings (maxRetries, retryInterval), refreshInterval and conditions (namespaceSelector, namespaces, namespaceRegexes). The operator reads conditions on a ClusterSecretStore only; a secretstore that authors them carries them.
    • externalsecret (ExternalSecretSpec): secretStoreRef (name, kind), target (name, creationPolicy, deletionPolicy, immutable, template, manifest), refreshPolicy, refreshInterval, syncWindows (kind, windows), data (each a secretKey, a remoteRef and a sourceRef) and dataFrom (each an extract, a find, a rewrite and a sourceRef).
    • clusterexternalsecret (ClusterExternalSecretSpec): externalSecretSpec (the fields of an externalsecret), externalSecretName, externalSecretMetadata (labels, annotations), namespaceSelector, namespaceSelectors, namespaces and refreshTime.
    • A duration is written in the form the API type gives it: refreshInterval: 1h is emitted as 1h0m0s, and so are a sync window’s duration and a clusterexternalsecret’s refreshTime.
    • No default is filled. The API’s defaults (an external secret’s refreshInterval, a target’s creationPolicy and deletionPolicy) are the API server’s to fill where the field was not authored. The kind of a secretStoreRef has 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 0 or false, except on three store fields, where the value is refused. A default the operator applies itself is not covered. The type omits a zero in provider.beyondtrust.server.decrypt, provider.infisical.secretsScope.expandSecretReferences and provider.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_DefaultedZeros holds 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.url and provider.kubernetes.auth.serviceAccount.name are 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 the name of a reference provider.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.version and provider.openBao.version (the API’s default is v2), provider.vault.auth.cert.path (cert) and provider.onepasswordSDK.cache.ttl (5m). A Vault store therefore writes its version: 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: its key and operator. Presence only: the operator’s value and its values are left to the API server (see “A label selector’s match expressions”). 2 paths, beside the generated ones.
    • An externalsecret has no required top-level field. Of what is authored: a data entry’s secretKey, remoteRef and its key; the key of an extract; the source and target of a rewrite’s regexp and the template of its transform; the kind and name of a generatorRef; of syncWindows its kind and windows, and a window’s schedule and duration; of a target.manifest its apiVersion and kind; of a ConfigMap or Secret a template is read from (target.template.templateFrom[]) its name, its items and an item’s key. 23 paths.
    • A clusterexternalsecret: externalSecretSpec, and under it everything an externalsecret requires; and the key and operator of a match expression authored in namespaceSelector or in an entry of namespaceSelectors, presence only. 28 paths.
    • The published schema marks the two top-level ones as required (go-kure/launcher#790): provider of a secretstore and of a clustersecretstore, and externalSecretSpec of a clusterexternalsecret. That refuses no component that built before, since the kind refused each without it already.

    TestExternalSecretsKinds_RequiredMatchSource holds 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 no omitempty and 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’s key and operator are derived, by the same rule on the source of the linked metav1.LabelSelectorRequirement; every other required field of such a type is not refused, and an omitted one is emitted empty. TestExternalSecretsKinds_StoreRequiredIsGenerated fails 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 with UPDATE_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 as null configures nothing, as on the API server;
    • a generator is not named as the source of one key of data. The API takes exactly one of storeRef and generatorRef in that sourceRef, and the type writes storeRef whether 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 of dataFrom takes a generator. On a clusterexternalsecret both paths are under externalSecretSpec.

    Not checked, and the API server’s to refuse:

    • the count rule on ten other types, each “exactly one of these fields”: a rewrite, the sourceRef of a dataFrom entry, and eight blocks of single providers (an authentication method, a reference, how a Yandex provider fetches an entry). The minimum of one on a data entry’s sourceRef is met by the storeRef the object always carries;
    • the six counts over named fields, on three providers: openBao (at most one of caBundle and caProvider; exactly one authentication method under auth; exactly one of roleId and roleRef on an app role; exactly one of serviceAccountRef and secretRef on its Kubernetes authentication), onepasswordSDK (at most one of vault and environment) and crd (at most one of auth and authRef). 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 data nor dataFrom, two data entries with one secretKey, a deletionPolicy its creationPolicy does not allow, a template of a bootstrap-token Secret or of a service-account-token Secret that names its service account, a dataFrom entry that sets none or several of its sources; a store whose namespaceRegexes do not compile, whose refreshInterval is no duration, or whose provider refuses its own configuration. Launcher repeats none of them, so an externalsecret with 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’s secretStoreRef and target, a clusterexternalsecret’s externalSecretMetadata, an AWS provider’s auth and a Vault provider’s tls, among others. The API accepts each. TestExternalSecretsKinds_WrittenUnauthored pins 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 value beside the secretRef that 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.certificateKey and .auth.clientSecret, provider.delinea.clientSecret, provider.scaleway.secretKey, provider.secretserver.password and .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.applicationCredentialID and .auth.username, provider.beyondtrust.auth.certificate and .auth.clientId, provider.delinea.clientId, provider.scaleway.accessKey and provider.secretserver.username. TestExternalSecretsKinds_InlineValues finds every string value beside a secretRef in the linked module (14 at this pin) and fails on one that is in neither list.
    • The data of the fake provider is refused under the same policy. provider.fake.data is 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: a fake provider with no data is 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_FakeData pins 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 webhook provider’s request, a header of a vault provider, the user part of a provider’s URL, and the template of the Secret an external secret writes (target.template.data, a templateFrom literal, 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. An externalsecret and a clusterexternalsecret name what is read and where it is written, and ApplyPolicy fills nothing on either. With target.manifest (an apiVersion and a kind) the operator writes an object of that kind instead of a Secret, from template text it renders in the cluster. The apiVersion has 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/ and Apps/v1 are 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 on passthrough. What passthrough would 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 a clusterexternalsecret the path is externalSecretSpec.target.manifest). The transform applies NoopPolicy when no policy is passed, so this holds then too. This is stricter than passthrough wherever 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. passthrough reads the object and finds nothing to refuse; here the content is not known at build, so there is nothing to read. A core Secret named there is refused under a policy that forbids explicit secrets and passes under any other, as on passthrough. Any other kind (a ConfigMap, a custom resource) passes as it would on passthrough, and whether the operator writes it at all is the cluster’s: the operator’s generic-target setting and its RBAC. The target path of a templateFrom entry needs no check of its own: it places text inside the object target.manifest names. TestPolicyReadsKind_IsWhatTheRenderedObjectCheckReads fails 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.connectHost and the others).
    • A nil policy checks nothing.

    The store of an external secret is the author’s. secretStoreRef names a store by name and kind; 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 a secretstore or clustersecretstore component serve an externalsecret component, name its object: the component name, or its objectName. The API reads a secretStoreRef without a kind as a SecretStore of the ExternalSecret’s namespace. The Secret target names is the operator’s to create, and the component emits none.

    Labels and annotations are the labels and annotations properties. 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 its externalSecretMetadata, and those of the Secret an external secret writes its target.template.metadata: the operator applies both, and launcher adds nothing to either.

    Beside the external-secret trait. 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’s external-secret capability, names it after its secretName, and mounts or injects the Secret into the workload. The externalsecret kind 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/v1alpha1 API: a ReplicationSource, which copies a volume out, and a ReplicationDestination, which receives one. Each is built on policyHeldKind (policyFreeKind with a policy check, see above) and emits that one object in the build namespace, named after the component unless objectName names 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; the volsync trait 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, or manual); the movers rsync, rsyncTLS, rclone, restic and syncthing; external, a provider outside VolSync with its parameters; and paused.
    • replicationdestination (ReplicationDestinationSpec): trigger, the movers rsync, rsyncTLS, rclone and restic, external and paused. A destination has no Syncthing mover and names no source claim: syncthing and sourcePVC are refused there as unknown keys.
    • rsync (rsync over SSH) has its own, shorter type: it takes moverResources, moverServiceAccount and moverPodLabels, and has no moverSecurityContext, moverAffinity or moverVolumes, 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 false or 0 is kept where the API tells it from an unset field (restic.retain.monthly: 0, a destination’s restic.previous: 0, a Syncthing peer’s introducer: false, moverSecurityContext.runAsUser: 0), and left out where the type omits a zero (paused: false). VolSync’s CRDs default no field under spec, so no authored zero is replaced by a CRD default; TestVolsyncKinds_NoDefaults holds 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, on rsyncTLS, rclone, restic and syncthing): mountPath and volumeSource (restic.moverVolumes[0].volumeSource: required (…)).
    • Of a Syncthing peer: address, ID and introducer. No default is filled for introducer: an authored false is a value.
    • Of a mover’s affinity that authors a required node affinity (moverAffinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution): its nodeSelectorTerms, which the Kubernetes type would write as null where the CRD requires them. TestKindComponents_NullRequired shows 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, on rsyncTLS, rclone, restic and syncthing): key and operator. Presence only: the operator’s value and its values are 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_RequiredMatchCRD holds 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 the key and operator of 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 key of 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.defaultUser and <mover>.moverVolumes[].volumeSource.secret.items[].user, an authored 0 included (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_AbsentFromCRD derives 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_NoExpressionRules holds 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 authored rclone or restic mover, which holds it by value. The CRDs accept it empty, and TestVolsyncKinds_RequiredMatchCRD holds every such field to its schema.

    Policy.

    • An authored capacity is held to the storage maximum. A mover’s capacity (on rsync, rsyncTLS, rclone and restic) 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.cacheCapacity sizes Restic’s metadata cache volume and, on a source, syncthing.configCapacity Syncthing’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 moverResources are 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 hostProcess switch is refused unless the policy allows privileged workloads. moverSecurityContext.windowsOptions.hostProcess: true runs the mover’s containers as Windows HostProcess containers: component "web": restic.moverSecurityContext.windowsOptions.hostProcess is not allowed by environment policy. An authored false builds. With no policy passed it is refused too, since NoopPolicy allows nothing privileged.
    • The rsync mover 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 authored rsync mover, 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.go in its module): a cluster that runs another version of the operator may add other capabilities, and the kind does not know. TestVolsyncKinds_RsyncCapabilities reads that literal and holds the non-test Go source of the two packages the container is built in (internal/controller/mover/rsync and internal/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 only windowsOptions.hostProcess is 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 an rsyncTLS, rclone, restic or syncthing mover 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. The rsync mover’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’s address, and the server of an NFS export among moverVolumes.
    • 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). The parameters of an external provider 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 volsync trait. 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. The replicationsource kind 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 a replicationsource component’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 labels and annotations properties. A mover’s moverPodLabels are 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’s serviceAnnotations reach 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 — serviceName and volumeClaimTemplates (name, mountPath or — for a volumeMode: Block claim — devicePath, size, storageClass, accessModes, plus the rest of corev1.PersistentVolumeClaimSpec; see “Raw block volumes” above). The StatefulSetSpec-level and claim-template field sets are classified in “StatefulSet-level and claim-template properties” below. ports declares the main container’s ports (see “Main container ports”). It emits no Service (go-kure/launcher#690). For scheduling it takes affinity, tolerations and topologySpreadConstraints as the raw corev1 shapes (go-kure/launcher#790, see “Raw scheduling properties” above); the four-key affinity shorthand is not published on this kind.

    • serviceName names the governing Service, which the component does not emit. Author it as a headless service component (clusterIP: None, selecting app: <component>) and name it here. It has no default: unset, the StatefulSet carries serviceName: "". 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); Generate applies the same rule to the config’s ServiceName. Before go-kure/launcher#690 serviceName defaulted 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, httproute or expose without a backend on it is refused (component "<name>" has no service port). Put the trait on the authored service, or name that Service as the trait’s backend.
  • daemonset — tolerations (key/operator/value/effect/tolerationSeconds; tolerationSeconds and the toleration cross-field rules arrived with go-kure/launcher#412 via the shared parser — see “What tolerations changed for daemonset” above, which is the only place in this work that is not additive); ports declares the main container’s ports (see “Main container ports”). It emits no Service (go-kure/launcher#690): author a service component selecting app: <component> to expose the pods, and put routing traits on it. Since go-kure/launcher#790 it also publishes the raw affinity and topologySpreadConstraints (see “Raw scheduling properties”) and sidecars, read, checked and enforced exactly as on statefulset: the same closed entry, the same pod-wide port-name check against the main container’s ports, and the same ApplyPolicy loop. A sidecar here runs on every node the DaemonSet schedules to. DaemonSetSpec-level (go-kure/launcher#340, daemonset_spec.go): updateStrategy, minReadySeconds, revisionHistoryLimit. appsv1.DaemonSetSpec has five fields; template is the pod projection above and selector is builder-managed, which leaves these three.

    Before go-kure/launcher#690, port > 0 added a ClusterIP Service named after the component, which held the component name to the Service-name rule and an ingress portName to http (go-kure/launcher#545, go-kure/launcher#546). With no Service, only the container-name rule applies (see webservice / worker).

    PropertyTypeEffectCompatibility
    updateStrategyobjecttype (required, RollingUpdate|OnDelete) and rollingUpdate (maxUnavailable, maxSurge), only accepted under type: RollingUpdate. type: RollingUpdate alone is accepted — the apiserver defaults the rollingUpdate object that upstream validation then requires.additive
    minReadySecondsint ≥ 0Seconds a new pod must stay ready before it counts as available.additive
    revisionHistoryLimitint ≥ 0Superseded 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

    maxUnavailable and maxSurge each 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’s IsNotMoreThan100Percent, which inspects only percentages). A leading sign ("+50%") is rejected — upstream’s own IsValidPercent form check is used rather than a TrimSuffix/Atoi pair, which would accept it. Unlike the statefulset kind, whose maxUnavailable “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 — maxUnavailable 1, maxSurge 0 (k8s.io/api/apps/v1/types.go, the RollingUpdateDaemonSet field docs). So maxSurge: 2 on its own is rejected here rather than at apply time (the defaulted maxUnavailable: 1 makes both non-zero), and using surge means writing maxUnavailable: 0 alongside 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 leaves Type empty, so a schema consumer that does not read Types still accepts both forms.

    Two rules here are deliberately stricter than upstream. The API accepts both shapes; what it does with them differs. updateStrategy.type is required here, where the API defaults it to RollingUpdate and acts on that — so a bare updateStrategy: {} is a legal document whose entire meaning comes from defaulting rather than from anything written. updateStrategy.rollingUpdate is refused under type: OnDelete, where ValidateDaemonSetUpdateStrategy’s OnDelete branch is empty and the field is accepted and never read — the silently-ignored knob this projection exists to remove. Both are still additive: updateStrategy is 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-nil rollingUpdate under RollingUpdate, but apiserver defaulting satisfies that, not the author, so type: RollingUpdate alone is accepted (the same reasoning the statefulset kind applies to its own optional rollingUpdate).

    Every accepted property is presence-gated: a document authoring none of them produces byte-identical output to before, because DaemonSetSpecConfig.apply writes only the fields that were authored. The one behavior change is selector: a selector: 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 because daemonset does not declare the key and because ValidateAuthoredProperties (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. An app.yaml, kurel.yaml, or cluster.yaml with unrecognised keys is a build error” — so a daemonset carrying selector: was never a valid launcher.gokure.dev/v1alpha1 document, 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 — schedule accepts 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; @reboot is rejected, meaningless for a CronJob), or @every <duration> (e.g. @every 1h30m, validated via Go’s time.ParseDuration — a malformed duration is rejected, not merely regex-matched). restartPolicy (default OnFailure), successfulJobsHistoryLimit/failedJobsHistoryLimit. ports declares container ports, and emits no Service (see “Main container ports”). No sidecars schema 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 raw affinity, tolerations and topologySpreadConstraints (see “Raw scheduling properties”), written onto the Job template’s pod template. CronJobSpec-level: concurrencyPolicy (Allow|Forbid|Replace; the API’s own default is Allow, but this is only ever written when authored — ConcurrencyPolicy has no omitempty, 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, Local is rejected case-insensitively even though Go’s own time.LoadLocation("Local") succeeds — Kubernetes’ CronJob validation rejects it as server-dependent — and any other value is checked via time.LoadLocation, which this binary resolves from an embedded IANA database rather than the host’s zoneinfo). JobSpec-level (projected onto spec.jobTemplate.spec, shared with the job component — 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. suspend on a cronjob is the CronJobSpec field, not the JobSpec one — see the suspend note in “Common config”. Every CronJobSpec-level and JobSpec-level field above except restartPolicy and 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, taking OnFailure, 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 its objectName (or the Naming hook’s answer), when the component is read and again at Generate: cronjob "<name>": the component name is the CronJob's name, which must be at most 52 characters: it has 53, or objectName (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-field schedule form accepts any 5 whitespace-separated tokens with no per-field semantic check (e.g. 99 99 99 99 99 builds 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 (see docs/oam/design-gvk.md).

  • job (go-kure/launcher#344, job.go) — a run-to-completion workload: image, restartPolicy (default OnFailure; the Job API rejects Always, 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. No port and no Service; ports declares container ports (see “Main container ports”), and only then may a probe or hook name a port; no sidecars schema key (init containers only), matching cronjob and for the reason given there. Since go-kure/launcher#790 it publishes the raw affinity, tolerations and topologySpreadConstraints (see “Raw scheduling properties”), written onto the Job’s pod template. It emits a Job and 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-name and batch.kubernetes.io/job-name labels (generateSelector), and a label value is at most 63. The rule is held on the name the Job takes, the component name or its objectName (or the Naming hook’s answer), when the component is read and again at Generate, in the words the cronjob rule 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: Indexed and completions above 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 as job "<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 leave completions unset on an Indexed job, but a JobConfig built in Go can: with Parallelism unset too the API server reads both as 1, so Generate holds the name to <name>-0 there.

    The twelve JobSpec-level properties are the ones cronjob projects onto its job template, projected here onto spec directly. Every one is presence-gated, so a document authoring none of them emits no key for them.

    PropertyTypeEffectCompatibility
    backoffLimitint ≥ 0Retries before the job is marked failed. API default 6, or 2147483647 when backoffLimitPerIndex is set.additive
    completionsint ≥ 0Successful pods required. Required under completionMode: Indexed.additive
    parallelismint ≥ 0Pods running at once. ≤ 100000 under Indexed; ≤ 10000 when completions > 100000 and backoffLimitPerIndex is set. 0 runs no pods at all.additive
    activeDeadlineSecondsint > 0Seconds the job may run. Must be positive — 0 is rejected, not just negatives. Distinct from the pod-level podActiveDeadlineSeconds.additive
    ttlSecondsAfterFinishedint ≥ 0Seconds after finishing before the job and its pods are deleted.additive
    completionModeNonIndexed|IndexedHow completions are counted. Indexed requires completions.additive
    backoffLimitPerIndexint ≥ 0Retries within one index. Requires Indexed.additive
    maxFailedIndexesint ≥ 0Failed indexes tolerated before the whole job fails. Requires backoffLimitPerIndex (and so Indexed); ≤ completions and ≤ 100000, or ≤ 10000 (and required) when completions > 100000.additive
    podReplacementPolicyFailed|TerminatingOrFailedWhen a replacement pod is created. An empty string is rejected rather than treated as unset, for the same reason as managedBy below.additive
    managedBystringController 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
    successPolicyobjectrules[] (1..20) of succeededIndexes (increasing comma-separated intervals, every index < completions) and/or succeededCount (≤ completions, and ≤ the number of indexes named alongside it). Requires Indexed. An empty succeededIndexes is 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
    podFailurePolicyobjectrules[] (0..20), each with an action (FailJob|FailIndex|Ignore|Count) and exactly one of onExitCodes (operator In|NotIn, values[] of 1..255 exit codes in increasing order without duplicates, optional containerName) or onPodConditions[] (up to 20 type/status patterns; an omitted or null status defaults to True, an empty one is rejected). An empty onPodConditions: [] alone is neither; beside onExitCodes it is both, as the rule’s published exclusive group counts it (go-kure/launcher#790). Requires restartPolicy: Never, and pins podReplacementPolicy to Failed when that is also authored. FailIndex additionally requires backoffLimitPerIndex.Behavior-changing on cronjob (see below); additive on job
    suspendboolJobSpec.Suspend — create the job with no pods. Not the same field as cronjob’s suspend; see the suspend note 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. manualSelector only has meaning alongside one. template is 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. cronjob refuses 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 job and cronjob (go-kure/launcher#790): alpha upstream, behind the WorkloadWithJob feature 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 cronjob document retyped to job leaves behind. Dropping them silently would run at apply time the work a schedule deferred (see below).additive (no valid job document ever carried them)

    Every cross-field rule above is ported from ValidateJobSpec (k8s.io/kubernetes/pkg/apis/batch/validation) at the pinned k8s.io/api version, 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 is Never, but ValidateJobSpec does 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’s updateStrategy strictness, 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 in k8s.io/api/batch/v1 is stricter than the validator that is supposed to enforce it. activeDeadlineSeconds: 0 is refused although the validator only requires non-negative, because the field doc says “value must be positive integer”. An empty successPolicy.rules[].succeededIndexes is 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 with podFailurePolicy below. 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 both job and cronjob, through the same shared parser as the rest of the table. Its rules are ported from validatePodFailurePolicy / 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 requires restartPolicy: Never alongside a podFailurePolicy (these components default it to OnFailure, so a document authoring only the policy is refused with a message naming the restart policy), and onExitCodes.containerName must name a container the template actually carries — the component’s own container or one of its initContainers. 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 in createJob reads the emitted name.

    One place this parser is deliberately no stricter than upstream: an empty rules list is accepted. validatePodFailurePolicy has no “at least one rule” check — unlike validateSuccessPolicy, which does — and PodFailurePolicy.Rules’ own field doc states only the cap, so unlike activeDeadlineSeconds and succeededIndexes above there is no documented contract to follow past the validator. An empty list is not inert either: a non-nil policy pins podReplacementPolicy and restartPolicy whatever its rules say.

    onPodConditions[].status defaults to True (go-kure/launcher#410): a pattern that omits the key, or sets it to null, compiles to an explicit status: "True", the value upstream’s SetDefaults_PodFailurePolicyOnPodConditionsPattern fills in before validation. It is written out rather than left to the API server because the field carries no omitempty, so an unset one would be emitted as status: "". 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 for True. The default is additive: it only makes previously refused documents valid, and every previously valid document compiles identically.

    Compatibility. On job this is additive — the component type is newer than the key. On cronjob it is behavior-changing, and the reason is worth stating precisely: before this change an authored podFailurePolicy on 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 authoring podFailurePolicy therefore either compiles to a CronJob that now carries the policy, or stops building — most likely on the restartPolicy: Never requirement, since the component defaults to OnFailure. Under the additive test in docs/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 build now calls ValidateAuthoredProperties (pkg/cmd/kurel/build.go:149), which checks declared keys and shapes on the authored document directly.

    The selector/manualSelector rejection is the same class of change the daemonset kind’s selector rejection 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 valid launcher.gokure.dev/v1alpha1 job. Here it is not even a tightening — job is 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, successfulJobsHistoryLimit and failedJobsHistoryLimit — are rejected for the same reason selector is, but the case they guard is sharper. A job component and a cronjob component differ by exactly these six properties, so retyping an existing cronjob document to job leaves 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 (activeDeadlineSeconds for startingDeadlineSeconds, ttlSecondsAfterFinished for both history limits), so the refusal is a redirection rather than only a “no”.

    The properties this component introduces read an explicit null as an omission: backoffLimitPerIndex: null means “leave this unset”, not “this is a value of the wrong type”. That matches pkg/oam’s own validatePropertyValue. So does completionMode, which the shared parseJobSpec read with a bare lookup until go-kure/launcher#570, on cronjob as well. Since go-kure/launcher#394 this is not special to this component: every optional-field helper in common.go routes its presence check through authoredValue, so each JobSpec field shared with cronjob that 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 job in place needs it force-replaced, which the force-replace trait asks for. A Job’s pod template is immutable: ValidateJobSpecUpdate runs validatePodTemplateUpdate on every update, and the only carve-out is for scheduling directives on a suspended Job. By default, changing this component’s image, command, env or 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-replace trait (no properties) to the component. It sets the ForceReplace delivery intent on the component’s application (go-kure/launcher#782). kure’s Flux workflow turns that into kustomize.toolkit.fluxcd.io/force: enabled on every object the component emits — the Job — which kustomize-controller reads as its apply ForceSelector: 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 when createJob clears the generated Job’s annotations wholesale (job.Annotations = nil — a no-op since go-kure/launcher#361, because kure’s Create<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 its type and properties, are read only for the tier annotation. The annotations property 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: true on the enclosing Flux Kustomization has the same effect for every resource it reconciles, and deleting the Job before redelivering avoids the error without either. cronjob does not have this problem: a CronJob’s jobTemplate is mutable, and each run creates a fresh Job.

    The generated Job carries app: <component> as its own labels and its pod template’s, carries no annotations of the handler’s, and leaves spec.selector unset for the job controller to fill. What the labels and annotations properties 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 generates spec.selector plus its matching controller-uid/job-name pod 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 (flux default | template), source (inline url with optional kind and, for a GitRepository, ref; an inline Bucket’s kind, endpoint, bucketName, provider, region, prefix; beside any of these a name for 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, hookGroupNamePrefix and layoutKustomizationName (each under delivery: template only, see below), and the HelmRelease keys interval, releaseName, targetNamespace, driftDetection, install, upgrade, valuesFrom.

    • delivery: flux emits a helmrelease under the authored name, with the authored traits and annotations. A HelmRepository, GitRepository or Bucket source (go-kure/launcher#336) becomes chart.spec.sourceRef with chart required: 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 sets chart.spec.reconcileStrategy: Revision, so a new source revision deploys even when the chart’s version is unchanged (Flux’s ChartVersion default would skip it); a HelmRepository keeps that default. An OCIRepository or HelmChart source becomes chartRef. values and the HelmRelease keys are forwarded verbatim, so their shape is the helmrelease terminal’s to check. valuesMode is never forwarded, and the rule has no registration-time default for it: inline (or no valuesMode) keeps values on the helmrelease.

    • valuesMode: configMap (go-kure/launcher#702) with non-empty values moves them into a configmap trait the rule appends to the helmrelease, after its authored traits, and drops values from it. The trait’s ConfigMap carries the values under the key values.json, and a valuesFrom entry {kind: ConfigMap, name: <it>, valuesKey: values.json} is placed before the authored entries, so an authored entry wins on a shared key (Flux merges spec.valuesFrom in 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; see valuesConfigMapName below). Empty or absent values add no trait and no entry; values that are not a JSON object, a non-finite number in them, or a valuesFrom that is not a list are helm: build errors. Being an ordinary configmap trait, it is checked by that trait (a key it refuses, or data over the ConfigMap size limit, fails the build), labelled app: <label value> like the trait’s other ConfigMaps, emitted after the HelmRelease, and moved with the release into the Flux namespace, since its valuesFrom names it. The trait builds its ConfigMap through the configmap kind’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 authored configmap trait on the same component that takes the same name is not refused, and both ConfigMaps are emitted.

    • valuesConfigMapName and valuesSecretName (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’s Naming hook (roles values-configmap and values-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 its interval, not at once. Flux reconciles at once only where the ConfigMap or Secret carries the label reconcile.fluxcd.io/watch: Enabled, or helm-controller runs with a --watch-configs-label-selector that selects it. The rule sets no such label.

      A property that names nothing is refused: valuesConfigMapName without valuesMode: configMap or with empty values, valuesSecretName with empty or absent secretValues, and either under delivery: 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’s Naming hook (role helm-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. helmReleaseName is the name of the HelmRelease object in the cluster (metadata.name); releaseName is the name Helm gives the release (spec.releaseName, the chart’s .Release.Name). Renaming the object moves nothing else: spec.releaseName stays the authored releaseName or the component name, so the installed release is not reinstalled under another name, the values ConfigMap and Secret keep their names, and the helmrelease member 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 authored helmrelease component whose objectName is 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). Under delivery: template the 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 like values; absent, null or empty it changes nothing. The helmrelease kind has no such property: there, name a Secret in valuesFrom.

      • Under delivery: flux the rule appends a secret trait to the helmrelease, after its authored traits and after the values ConfigMap’s trait. The trait’s Secret holds the tree under the key values.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). A valuesFrom entry {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 of secretValues is written to the HelmRelease or to the values ConfigMap. Being an ordinary secret trait, it is checked by that trait (the 1 MiB limit), labelled app: <label value>, emitted after the HelmRelease, and moved with the release into the Flux namespace. A change in secretValues renames the Secret and so changes the HelmRelease, which is what makes Flux upgrade the release at once (under this default name; see valuesSecretName above).
      • Under delivery: template the tree is passed to the helmtemplate, which merges it over values for 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 values and secretValues is 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. Under delivery: template the 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 a map[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 inline spec.values after every valuesFrom entry.
      • A key named global below the top level of secretValues is 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>.global as that dependency’s globals. Where their shape conflicts with the chart’s own global at 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 under delivery: template, in the Flux controller’s under delivery: flux. Launcher does not read the chart, so it cannot tell a dependency from any other key: a key named global below the top level that is not a dependency’s globals cannot be given through secretValues either, only through values. Put a sensitive global under the top-level global, 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 the pkg/oam README) refuses a component with secretValues, 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. Under delivery: template an error about an object the chart rendered is another matter (see helmtemplate, “not covered”).

      Security note. secretValues keeps 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 (data is 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 (an external-secret trait, a sealed or externally managed Secret) and name it in valuesFrom, or use the chart’s own existing-secret values.

    • An inline source also emits the source: a helmrepository with only the URL for http(s)://, or an ocirepository with ref.tag: <version> for oci://. A Git repository needs kind: GitRepository set (an http(s):// URL alone means a Helm repository) and emits a gitrepository with the URL (http:// or https://) and source.ref, which must set exactly one of branch, tag, semver, name, commit: Flux would otherwise check out branch master, or pick one of several fields by precedence. The schema publishes the rule as an exclusive group on source.ref, and a field authored as an empty string beside a set one counts as a second field. kind: Bucket with endpoint and bucketName (and optionally provider, region, prefix), and no url, emits a bucket with exactly those keys. Credentials (secretRef and the like) have no inline form: author the gitrepository or bucket component and reference it. That includes every ssh:// repository, which Flux reads only with a key from secretRef. The ocirepository also selects the Helm chart content layer (application/vnd.cncf.helm.chart.content.v1.tar+gzip) with operation: 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>, or bucket:<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 release interval. 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’s Naming hook, 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.name beside 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.name equal 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 the helmrepository component with objectName and 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.name alone, with no inline source, references an existing source as before, and source.namespace belongs to that form only.

      Breaking (go-kure/launcher#787). A helm component with both source.url and source.name was refused and builds now, under delivery: flux. The refusal of source.namespace beside an inline source reads helm: 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 by pkg/oam as a duplicate component name, and is now refused by the rule with the message above.

    • 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), and pkg/oam puts 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. A placement policy or tier annotation naming the generated source, and a dependency rule making it wait on a component, fail the build. A source the component references by source.name is the author’s own component and the rule orders nothing after it.

    • delivery: template emits a helmtemplate with the URL, its resolved kind, chart, version, values, secretValues, scopeOverrides and an authored releaseName, which the helmtemplate checks and, when unset, defaults to the component name, the release name the delivery: flux HelmRelease carries too (go-kure/launcher#785; see helmrelease). An authored hookGroupNamePrefix (go-kure/launcher#787) is passed on as written and is the helmtemplate’s; under delivery: flux it is refused with a helm: message, since a HelmRelease installs the chart, no hook-group layout exists and the prefix would name nothing. An authored layoutKustomizationName (go-kure/launcher#787) is passed on and refused the same way: under delivery: flux the component has no layout of its own. No source is emitted, and an authored valuesMode: inline is dropped. The rule refuses everything a client-side render cannot honour, each with a helm: message:

      • a source reference, and valuesMode: configMap;
      • source.name beside an inline source, valuesConfigMapName, valuesSecretName and helmReleaseName (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), which helmtemplate does not accept.
    • scopeOverrides (go-kure/launcher#794, item 11) states the scope of a kind the chart renders, for delivery: template: the list of {apiVersion, kind, scope} the helmtemplate and manifests components take, with the meaning it has there (see Scope overrides under helmtemplate). The rule forwards the authored list to the helmtemplate as written, under the declared spelling of the key, so a chart delivered through helm and one authored as a helmtemplate with the same list render the same objects. null reads 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 a CustomResourceDefinition the chart renders is refused when the chart is rendered, by the helmtemplate.
      • delivery: flux refuses 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.
    • Strict, unlike the removed helmchart composite. An undeclared key at the top level or inside source is refused, and so are:

      • delivery: native;
      • source.namespace with 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 the secretRef of an authored helmrepository or ocirepository, referenced with source.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.ref field, or with a URL that is not http:// or https:// (an ssh:// 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;
      • url with an inline Bucket, or one without endpoint or bucketName, or an endpoint that is more than a host[:port] or an https:// URL of a host and port (user info, a path, a query, a fragment or http:// is refused: a non-TLS endpoint needs insecure: true, which only an authored bucket takes) or whose port is outside 1–65535, or a provider other than generic, aws, gcp or azure (checked at the helm rule; the bucket terminal leaves it to the CRD);
      • source.ref other than on an inline GitRepository, and endpoint, bucketName, provider, region, prefix other than on an inline Bucket;
      • chart with an OCIRepository or HelmChart source;
      • version with a referenced OCIRepository or HelmChart source, which pins its own;
      • version with a GitRepository or Bucket source: Flux reads that chart at the source’s fetched revision and ignores version;
      • two keys equal ignoring case, at the top level or inside source (chart and Chart): 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/oam refuses 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; use helm. A document written for the composite fails validation on a key HelmChartSpec does not declare. When that key is one of the composite’s own that HelmChartSpec lacks (delivery, releaseName, targetNamespace, source, values, valuesMode, driftDetection, install, upgrade, valuesFrom), the error adds a pointer to helm; any other unknown key gets the plain error. To migrate, change type: helmchart to type: helm and rename delivery: native to delivery: 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 last helmchart output of each input, frozen before the removal, is diffed against the helm build, and the diffs are in pkg/cmd/kurel/testdata/helm-parity/:

    #What changes with helm
    1The values ConfigMap name carries a values hash.
    2A generated source is named <app>-source-<digest>, not after the component.
    3(void) oci and Helm-over-OCI components: neither shares an OCIRepository with the other (go-kure/launcher#665).
    4The delivery value native is flux.
    5(void) targetNamespace under a Flux namespace: both default it to the application namespace (go-kure/launcher#625).
    6A generated source keeps its terminal’s default interval, not the release interval.
    7No registration-time valuesMode default; valuesMode is never forwarded (under configMap the values become a configmap trait).
    8(void) Generated sources: neither gets an automatic health check; launcher sets none (go-kure/launcher#781).
    9The registry allowlist applies to inline sources.
    10Generated sources are applied with the application bundle, ahead of every group. A tier annotation on the component places only its release.
    11Template delivery refuses targetNamespace, and an unset releaseName renders under the component name, shortened as Flux shortens a release name, instead of release (go-kure/launcher#776, go-kure/launcher#785).
    12chart is refused with an OCIRepository or HelmChart source.
    13version is refused with a referenced OCIRepository or HelmChart source.
    14source.namespace is refused together with url.
    15Unknown 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).
    17placement may not place a generated source in any tier, and a dependency rule may not make it wait.
    18Any other helmchart default or build-time check a terminal does not reproduce (strict decoding).
    19The 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).
    20Every HelmRelease carries spec.releaseName: the authored value, else the component name, where helmchart left an unset one to Flux (go-kure/launcher#785).

    Two helmchart behaviours have no helm counterpart beyond row 18: valuesMode: configMap no longer makes the component a LayoutAugmenter (the values ConfigMap comes from a configmap trait on the helmrelease, ordinary build output, so kurel build accepts it and a layout-walking consumer no longer moves the component into a sub-layout), and under delivery: template the render’s .Release.Namespace can 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 of HelmReleaseSpec in the helm-controller API version go.mod links; 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.sourceRef or chartRef names one that already exists.

    Decoding. The whole property map is decoded with builtin.DecodeStrictJSON into HelmReleaseSpec. A key the struct does not declare, at any depth (chart.spec.chartVersion, a stray key inside a valuesFrom entry), 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. valuesMode is refused like any other unknown key; the error adds a pointer to the helm component’s valuesMode: configMap, or a configmap trait plus a valuesFrom entry, which replace it (go-kure/launcher#702). Known gap, inherited from the decoder: inside a type with its own UnmarshalJSON unknown keys are not refused — values is the case that matters, and it is open by design. Keys match case-insensitively, as in encoding/json; schema validation, which a kurel build runs first, is exact.

    Defaults and checks. interval defaults to 60m when unset (a zero duration counts as unset); Flux requires the field. A set interval must be a duration Flux accepts (go-kure/launcher#590, go-kure/launcher#601; oci applies the same check). The CRDs require ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$: unsigned, in ms, s, m or h (10m, 1h30m, 1.5h). time.ParseDuration alone would also accept -5m or 500us, which would fail at apply time; both are build errors. The value is emitted as a metav1.Duration, which serializes Duration.String() rather than the authored text, so that form is checked too: 0.5ms matches the pattern but is written as 500µ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 as 0s. In practice any value of 0s or at least 1ms is 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 in Generate on the decoded duration’s emitted form, for a config built directly. It lives in the internal pkg/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, and install.strategy.retryInterval and upgrade.strategy.retryInterval. A nested key matches case-insensitively at every level, as the decode does. Exactly one of chart and chartRef is required. The published schema lists both as optional and says so in the handler’s exclusive group (go-kure/launcher#790): a validator built from it refuses both and neither, as the build does. Under an authored chart, chart.spec.sourceRef.kind is required (go-kure/launcher#790): the API requires it and takes HelmRepository, GitRepository or Bucket, 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 with chart.spec.sourceRef.kind: required (…). Which of the three it is stays the CRD’s to check. An authored chart.spec.verify that names no provider is written with provider: 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 authored provider: "" 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 (Provider and provider) are refused, since the decode keeps one value and drops the other. TestKindComponents_OmittedRequiredAndWrittenDefaults holds 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. values must be a JSON object; a non-finite number (.nan, .inf) is a build error, never a panic. Each valuesFrom entry is held to the ValuesReference CRD’s own constraints, and a violation is a build error naming the entry’s index: kind is Secret or ConfigMap, exactly as Flux’s enum spells them (go-kure/launcher#748); name is required and at most 253 characters; a set valuesKey is at most 253 characters and matches ^[\-._a-zA-Z0-9]+$; a set targetPath is 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’s maxLength does, not bytes. Like the other checks these run again in Generate, and they cover the valuesFrom a helm component passes through. Nothing else is checked here: the enum checks on driftDetection.mode, install.crds and upgrade.crds are left to the HelmRelease CRD’s own admission, and so are the limits of an authored releaseName.

    Release name (go-kure/launcher#785). A component that authors no releaseName gets spec.releaseName set 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.ShortenName at oam.ShortenLimitHelmRelease, go-kure/launcher#793) — so the written name is the one Flux would shorten the component name to itself. An authored releaseName is written as it is, never shortened. helmtemplate renders under the same default, so a helm component releases its chart under one name with delivery: flux, with or without a Flux namespace, and with delivery: template. A default Helm would not install under is a build error with the remedy to set releaseName: the shortening can cut a dotted name just after a ., leaving a label that starts with -. The check runs again in Generate, where a HelmReleaseConfig built directly with neither a Name nor a releaseName is 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> when spec.targetNamespace is set and <name> otherwise, so the release name itself changes wherever a target namespace is set and no releaseName is authored: under a Flux namespace, which defaults one (below), and with an authored targetNamespace alone. There the release shop-web of a component web with target namespace shop becomes web. 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.releaseName in Flux’s HelmRelease reference). To keep the installed release, author its name as releaseName on 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; the name of an entry in the HelmRelease’s status.history). The long form is not accepted there: the HelmRelease CRD limits releaseName to 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 author targetNamespace gets spec.targetNamespace set 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 author storageNamespace gets spec.storageNamespace set to the effective target namespace, authored or defaulted, so the sh.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. A helm component takes no storageNamespace, so its release always gets the default. The Kustomization oci emits has no such default, deliberately (go-kure/launcher#794, item 1): a HelmRelease’s targetNamespace only says where the release installs, while a Kustomization’s overrides the namespace of every namespaced object in the artifact (see oci). The ConfigMaps and Secrets the HelmRelease reads from its own namespace — valuesFrom, kubeConfig.secretRef / configMapRef, and chart.spec.verify.secretRef when helm-controller creates the HelmChart beside the release (chart.spec.sourceRef names no namespace, or names the Flux namespace) — must live in the Flux namespace too. A configmap, external-secret or certificate trait 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 in valuesFrom therefore 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.releaseName is always set (see Release name), so Flux does not derive <targetNamespace>-<name>. Author releaseName when 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 storageNamespace gains spec.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 (its ReleaseTargetChanged check), so the release’s objects are deleted and recreated and its history starts over. To keep an installed release in place, author storageNamespace as the Flux namespace on a helmrelease component. Output without a Flux namespace is unchanged.

    Values in a ConfigMap. The HelmRelease is the only object helmrelease emits. Until go-kure/launcher#702 it took a launcher-owned valuesMode: configMap and emitted the values ConfigMap itself; that key is gone (see Decoding). The helm component’s valuesMode: configMap produces the same ConfigMap through a configmap trait (see helm), and on a helmrelease authored directly the same result is a configmap trait plus a valuesFrom entry 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 helm rule lowers to under delivery: 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 no HelmRelease.

    Properties. source is required: url (required) is an http:// or https:// Helm repository URL, or an oci:// URL naming the chart; kind (optional, HelmRepository or OCIRepository) is inferred from the scheme when unset — oci:// is OCIRepository, anything else HelmRepository — and must agree with it when set. A url carrying a user or password is refused, since the render takes no credentials, and the message never repeats it. chart is required for a HelmRepository; an OCIRepository’s URL already names the chart, so there chart is not used. version is required for an OCIRepository. values is 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 the helm rule’s inline source rather than copied. scopeOverrides states 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 under FluxIntegratedPerLayout; so does a prefix the Naming hook returns for the hook-group role. 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 the helm rule forwards its own secretValues as. The chart is rendered with secretValues merged over values, 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 the helm component states;
    • a key named global below 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 the helm component states and with its trade-off: such a key that is not a dependency’s globals can be given through values only, and a sensitive global goes under the top-level global;
    • a policy that forbids explicit secrets (oam.ExplicitSecretPolicy) refuses the component in ApplyPolicy, 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 the passthrough and manifests components 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 with values alone. If that fails too, its error is the one reported, after rendering 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 only rendering 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 in values. 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 scopeOverrides entry 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-level global it is the dependency’s global entry from values, 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 default slog logger.

    Release name. releaseName is the render’s .Release.Name. Unset, it is the default the helmrelease terminal writes to spec.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. A helm component therefore renders under delivery: template with the same release name its HelmRelease is installed under with delivery: 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 set releaseName, as Flux would fail to install it. A HelmTemplateConfig built directly defaults it from Name the 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 same releaseName in one namespace generate the same objects when the chart names its objects after the release, as most do, and kurel build refuses them as a generated-object collision rather than merging them. Before go-kure/launcher#776 every render used kure’s default release; the project is pre-GA, so the changed object names of an existing template-rendered chart carry no compatibility shim.

    Decoding. values, secretValues and scopeOverrides are split off, and the rest of the property map is decoded with builtin.DecodeStrictJSON into a closed struct, so any other key, at any depth, is refused by name, as is a wrongly typed value. values itself reaches the render exactly as authored, with its YAML-decoded value types, rather than the strict decoder’s json.Number re-reading, which a chart template comparing a value with a number would treat differently; secretValues likewise. A secretValues that is not an object is refused by its type, never by its content. The schema declares the same keys, with source closed to url and kind, and a test ties it to the struct. Keys match case-insensitively in the handler, as in encoding/json; schema validation, which a kurel build runs first, is exact.

    Refused outright. targetNamespace, every property only a Flux-reconciled release reads (interval, driftDetection, install, upgrade, valuesFrom, valuesMode), the helm rule’s delivery switch, and a source reference (source.name, source.namespace) are undeclared keys: schema validation refuses each, and so does the strict decode. An OCIRepository source without version is refused by the handler. The key itself is what is refused, whatever it holds. The render’s .Release.Namespace is the application namespace; the terminal declares no targetNamespace, so it cannot be set.

    Namespace. A namespaced rendered object that carries no metadata.namespace is 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 the manifests component resolves it, by the one function both call (resolveObjectScope, manifests.go): a scopeOverrides entry for the object’s kind, else kure’s scope table (manifest.Scope) — the kinds kure registers, in any API version — plus the scope a CustomResourceDefinition among the emitted objects declares for the kind it defines. Unlike manifests, 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 scopeOverrides entry. Lease, EndpointSlice and 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’s targetNamespace, a client’s default namespace). A chart’s crds/ 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 HelmTemplateConfig built directly with an empty Namespace has none to give and stamps nothing.

    Scope overrides (go-kure/launcher#794, item 11). scopeOverrides states the scope of a kind the chart renders, for a kind of unknown scope above: a list of {apiVersion, kind, scope}, scope being Cluster or Namespaced, matched on the exact apiVersion and kind. It is the property manifests has, 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 a CustomResourceDefinition among 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 without metadata.namespace is 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, which manifests refuses: template delivery refuses nothing about what a chart wrote.
    • An entry for a kind the chart does not render does nothing. null reads as omission.

    Refused, two cases only. A malformed entry, at decode and before any render, with the manifests messages under this component’s prefix (helmtemplate: scopeOverrides[0]: scope "cluster" is invalid; must be "Cluster" or "Namespaced"). And an entry that contradicts a CustomResourceDefinition the 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. A HelmTemplateConfig built directly holds the overrides in ScopeOverrides, where a value other than the two scopes is refused before the render.

    The helm rule takes the same property under delivery: template and forwards it here as written, after refusing a malformed entry under its own prefix; under delivery: flux it 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.namespace in 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, the admissionregistration.k8s.io/v1 kinds 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) gain metadata.namespace when the chart left it unset. A chart that renders a v1 List or 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 a helm.sh/hook annotation 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, manifests and crd alike. Built before, refused now:

    DocumentBeforeNow
    A list (a v1 List, 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/hook there, and still does, with its own textList has metadata of its own that its items cannot keep: annotations …; labels …
    Kind or apiversion beside 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 whose Kind or apiversion states 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 it
    Items on a v1 ListDecoded, to no object at all: a chart or a source with other objects built. A manifests or crd source that held nothing else was refused already (source resolved to no manifests)The same text, for "Items"
    Items on a list of an unregistered kindBuilt: the list emitted as one objectThe same text
    A null item in a list of an unregistered kindBuilt: an otherwise empty object emitted, with the list’s apiVersion and its kind without Listitem 0 of WidgetList: the item is null, not an object
    An item of a registered kind, in a list of an unregistered kind, that does not decode as its type (data: 1 on 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 decodeThe 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 apiVersion or kind and then null for 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 unreadableitem 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-level items arrayBuilt: the entries emitted in its place, each unstructured… an items array 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 v1 List, 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 null item of a v1 List was nil runtime object provided and is the item is null, not an object; … is nested more than 8 lists deep now holds for a list of an unregistered kind too; the policy check’s refusal of an object with a top-level items array no longer says the object sits 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/hook annotation (or a standalone helm.sh/hook-weight) is grouped by (phase, weight) via kure’s helm.SplitByHookWeight. Generate returns 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 as pre-install,pre-upgrade placed 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/test objects are dropped outright (kure does not surface them as static manifests) — mostly a bug fix, since a test Pod 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/oam has no logging channel to flag it.

    Rendered objects. The render is decoded with kure’s manifest parser (io.ParseYAMLWithOptions, unstructured allowed), the decode the manifests component 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 true as an annotation value) is a build error;
    • YAML is read as YAML 1.1, so an unquoted yes or y is 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: 9007199254740993 and 9223372036854775807 are written as rendered (they were written 9007199254740992 and 9223372036854776000). 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): 9223372036854775808 is written 9223372036854776000, and 18446744073709551615 as 1.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 without apiVersion and kind are 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 v1 List, a typed list such as DeploymentList), and a kind the scheme does not register whose name ends in List and that states items. Each item of a typed list is decoded as the kind the list holds. Each item of a v1 List and 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 leaves apiVersion or kind out is read with the list’s apiVersion and the list’s kind without List. An item that does not decode, or is null, 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 in List and states no items is one object; a registered list kind that states none is a list without items, and yields no object;
    • apiVersion, kind and, on a list, items are 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-level items array ({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": an items array 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 envelope passthrough refuses (Lists, under passthrough), and the one refused on a registered kind that declares no items (Undeclared fields, below). An items that 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’s oam.ViolationError, as every render that does not decode is, since the chart is rendered at the transform’s policy step; a manifests source 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 type an icmps field of a CiliumNetworkPolicy or a CiliumClusterwideNetworkPolicy left 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 BGPPeer at metallb.io/v1beta2, the version MetalLB stores, is one, beside v1beta1. 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, so spec.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/hook annotation is involved is a build error naming the list: the annotation on the list’s own metadata, or on one of its items (for a v1 List and 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 reads metadata under any case of the key and the parser reads the exact key, so a hook on a list under Metadata is 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: …. Generate returns a fresh copy of the decoded objects on every call, as manifests does, so a trait that decorates a typed workload (topology-spread, security-context, a mounted configmap, external-secret) acts on a chart’s Deployment as on an authored one, and generating the same result twice gives the same output.

    Policy. ApplyPolicy holds the chart to the environment policy in two steps (go-kure/launcher#791); the transform calls it with NoopPolicy when no policy is passed.

    • The source, before any fetch. The host of source.url must match an entry of the policy’s allowed registries (AllowedRegistries) exactly, as for oci, crd and manifests; for an oci:// 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 :latest one 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 the persistentvolumeclaim and statefulset kinds hold theirs. The replica count of a Deployment, StatefulSet, ReplicaSet or ReplicationController (one when the chart sets none) and the maxReplicas of a HorizontalPodAutoscaler, in any API version, are held to the replica maximum (MaxReplicas), as the deployment and statefulset kinds and the scaler trait hold theirs. A PersistentVolume is held to what the persistentvolume kind holds its own to: a hostPath or local source needs AllowHostPathVolumes(), and spec.capacity.storage is 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 NoopPolicy denies all five. The policy accessors that allow such a chart are AllowPrivileged(), AllowHostNetwork(), AllowHostPID(), AllowHostIPC() and AllowHostPathVolumes(); 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, crd and passthrough therefore 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 maxReplicas from the unstructured object as it does from the Go type. One shape is refused: a top-level items array 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 v1 List, 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 left apiVersion and kind out 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’s apiVersion and its kind without List. 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 apiVersion or kind and then null for it. The parser reads the last statement, takes the null for the key left out and gives the item its list’s; the strict decode keeps the string the null follows. 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’s apiVersion, 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 items schema is such a type: the same kind of type as a CiliumNetworkPolicy’s endpoint selector, which the kind inventory records and an authored Cilium policy checks on its own. A key next to properties in 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 of managedFields; 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-level items array (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 direct ApplyPolicy(nil), or Generate on a config no policy was applied to) checks nothing, and ApplyPolicy(nil) withdraws no policy applied before. A chart delivered as a Flux HelmRelease (helmrelease, helm under delivery: 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.LayoutAugmenter path), more than one hook group makes AugmentLayout clear the component’s flat Resources and replace them with one child ManifestLayout per group, named <application>-<component>-NN-<phase-slug>, written to its own directory <component dir>/<child> (its Namespace is 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 via DependsOn, listing each child’s preceding sibling in the order kure’s helm.SplitByHookWeight synthesizes from Helm’s hook phases — a combined install/upgrade ordering for GitOps reconciliation, not Helm’s own per-operation execution order (kure pkg/stack/helm/hooks.go:28-36). DependsOn is set on every child, and only kure’s FluxIntegratedPerLayout placement carries it: there kure’s layout integrator gives each child a Flux Kustomization CR of its own and writes each entry into its spec.dependsOn as the name of the Kustomization of the sibling the entry names (kure pkg/stack/layout/manifest.go’s DependsOn field doc). Under FluxSeparate and FluxIntegratedPerBundle a child gets no Kustomization of its own, and kure’s Flux integration refuses the child’s DependsOn and KustomizationName, 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 needs FluxIntegratedPerLayout under kure’s Flux integration. When it partitions, AugmentLayout also sets the component layout’s ApplicationFileMode to AppFilePerResource unless the caller already set one, so the component stays a directory whose kustomization.yaml lists the children. Without that, a writer-wide AppFileSingle default (kure layout.Config.ApplicationFileMode) would make kure’s WriteManifest refuse the tree under any placement but FluxIntegratedPerLayout: an AppFileSingle layout writes no kustomization.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’s kustomization.yaml lists it; under FluxIntegratedPerLayout kure’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’s AugmentLayout is a no-op. Every helmtemplate component is a LayoutAugmenter, 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 component db get <application>-db-NN-<phase-slug> children that differ. The transform hands the config its application (oam.ApplicationNameSetter, the Application field); a HelmTemplateConfig built 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 under FluxIntegratedPerLayout is <bundle's Kustomization>-<child> (shop-shop-db-01-main), and the next group’s spec.dependsOn follows 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, which helm uses 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 application a-b with component c and application a with component b-c still 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: the hookGroupNamePrefix property, or the Naming hook’s answer for the hook-group role (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 to oam.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); AugmentLayout returns 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; another hookGroupNamePrefix on one of the components is the way out (pkg/oam/README.md, “Name roles and the Naming hook”). A HelmTemplateConfig built directly sets HookGroupNamePrefix. The component’s own layout, the parent of the hook-group children, also gets a Flux Kustomization under FluxIntegratedPerLayout, 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 and AugmentLayout set it on the layout, so such a layout’s Kustomization name changes. The layoutKustomizationName property, or the Naming hook’s answer for the layout role, 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). A HelmTemplateConfig built directly sets LayoutKustomizationName; AugmentLayout never overwrites a KustomizationName the layout already carries. GenerateCoversAugmentLayout is always true — Generate’s output is already the flat union AugmentLayout repartitions — so kurel build, which never walks a layout, accepts the component and emits Generate’s flat output.

  • 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 or sha256:…), path, prune, interval, targetNamespace, wait, healthChecks.

    Lowering (go-kure/launcher#784). oci is a role-named component: OCIRule lowers it to the two kind-named terminals it stands for, an ocirepository (url, ref.tag from version, or ref.digest for a sha256: value, and the authored interval) and a fluxcd-kustomization (path, prune, sourceRef, and interval, targetNamespace, wait and healthChecks when set). No handler is registered for oci. The rule runs the checks below first, with their oci: 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 kustomizationName or source.objectName names them, below) and deploy as one unit, the OCIRepository first. This is the pair of objects oci has always emitted, byte for byte. Annotations go to both members, so a tier annotation, a placement policy or a dependency rule naming the component acts on the pair. prune-protection and force-replace cover both objects (the pair’s application takes the delivery intent); every other trait goes to the Kustomization.

    • Two or more oci components 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, through LoweringContext.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 source helm generates, it is then a generated source: pkg/oam applies 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’s Naming hook may rename it: it is asked under role helm-source, once for each source identity of a document, with that default and no component (go-kure/launcher#787).

    • source.name names 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-protection and force-replace then 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.

    • kustomizationName and source.objectName name 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’s Naming hook’s answer (roles oci-kustomization and oci-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, a placement policy or a dependency rule naming the component still acts on both, and the traits go where they went. The Kustomization’s sourceRef names the kept source by the name that object takes. Every one of these names is claimed, the default included, so an authored ocirepository or fluxcd-kustomization component whose objectName is the same name is refused in the transform with both named:

      name collision: OCIRepository.source.toolkit.fluxcd.io "default/manifests" is named by component "manifests" (role "oci-source", its default) and by component "repo" (role "object", set by properties.objectName); give one of them another name

      source.objectName and source.name both name the OCIRepository, in two ways, and are refused together. Write source.objectName to rename the source the component keeps to itself: it stays with the Kustomization and carries the component’s annotations and its prune-protection and force-replace traits. Write source.name for a source generated on its own, which components that write the same name share. A component that writes source.objectName keeps 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, the version and the effective interval. Unset, 0s, 60m and 1h are 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. An oci and a helm component 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 oci components 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’s prune-protection or force-replace no 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: 0s now emits the 60m default on both objects, since both terminals read a zero interval as unset; it used to be emitted as 0s. Nothing else changes.

    Sharing is decided among the oci components the document holds when the rule runs: an oci component 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 the ocirepository terminal’s check, so its refusals name that kind and its field (ocirepository: url: …) rather than oci: source.url.

    targetNamespace has no default (go-kure/launcher#622). Unset, the Kustomization emits no spec.targetNamespace and each object keeps the namespace the artifact’s own kustomize build gives it: the one it carries, or a namespace set in the artifact’s kustomization.yaml. kustomize-controller does not fill in its own namespace for an object that still has none. Author targetNamespace when namespaced objects are left without a namespace after that build: they otherwise fail at apply with namespace not specified, with or without a Flux namespace. This deliberately differs from helmrelease, which under a Flux namespace defaults targetNamespace to the application namespace. A Kustomization’s targetNamespace sets 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. wait and healthChecks are opt-in readiness settings for the delivery Kustomization (go-kure/launcher#432). A document in which neither requests anything — wait absent or false, and healthChecks absent, null or empty — builds the same Kustomization byte for byte, and the default stays unset. wait: true sets spec.wait, so Flux reports the Kustomization ready only once everything it applied is ready; false, like an omitted key, emits nothing. healthChecks is a list of {apiVersion, kind, name, namespace} entries copied, in authored order, into spec.healthChecks. The component delivers an opaque artifact, so the list is authored, never derived. apiVersion (apps/v1, or v1 for a core kind), kind and name are required non-empty strings, and namespace is 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: true together with a non-empty healthChecks is refused: kustomize-controller ignores healthChecks when wait is true, so the list would never be checked. There is no timeout property; Flux’s own default applies. Under a policy with a non-empty registry allowlist (AllowedRegistries), source.url must name its registry explicitly — oci://<registry>/<repository> with a non-empty repository and a registry that is localhost or 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’s name.NewRepository, which reads any other first segment as part of a Docker Hub repository: oci://ghcr.io and oci://registry/my-artifact are pulled from Docker Hub, so they are refused even when ghcr.io or registry is listed. No policy, or an empty allowlist, accepts every oci:// url. interval (default 60m) must be a duration Flux accepts, checked exactly as for helmrelease (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 of KustomizationSpec in the kustomize-controller API version go.mod links; 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, like helmrelease: it does not deliver the application it is part of, and it creates no source — sourceRef names one that exists, authored beside it (ocirepository, gitrepository, bucket) or already on the cluster. The type is fluxcd-kustomization rather than kustomization, which would read as the kustomization.yaml file (go-kure/launcher#352).

    Decoding. The whole property map is decoded with builtin.DecodeStrictJSON into KustomizationSpec, so patches, postBuild, dependsOn, force, timeout, serviceAccountName, decryption, kubeConfig and 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 declares kind, name and namespace in several places, so the bare key would not say where. A wrongly typed value is refused too. Keys match case-insensitively, as in encoding/json; schema validation, which a kurel build runs first, is exact.

    Defaults and checks. interval defaults to 60m when unset (a zero duration counts as unset); Flux requires the field. interval, retryInterval and timeout, when set, must be durations Flux accepts, checked as on helmrelease. sourceRef.kind is required and one of OCIRepository, GitRepository, Bucket, ExternalArtifact, the CRD’s enum; sourceRef.name is required. prune is a required field of the spec with no default here: unset, false is emitted (oci passes true unless told otherwise). targetNamespace is 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, and wait: true beside healthChecks, which oci refuses, is emitted as authored (kustomize-controller then ignores the list). Whether the source exists is known only on the cluster. The checks run again in Generate, 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 / configMapRef and each postBuild.substituteFrom entry — must live there too: a configmap, external-secret or certificate trait on the component whose object one of them names moves with the Kustomization (FluxNamespaceReads, go-kure/launcher#740). ApplyPolicy checks 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 version go.mod links — 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 (compare oci), no HelmRelease (compare helmrelease), no source for a helmchart (its sourceRef names one that exists, as a helmrelease’s chart.spec.sourceRef does), and no source dedup — two components naming the same URL emit two CRs. A helmrelease consumes a helmchart through chartRef with kind: HelmChart.

    Decoding. The whole property map is decoded with builtin.DecodeStrictJSON into 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 GitRepository include entry), 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 in encoding/json; schema validation, which a kurel build runs first, is exact.

    Checks and defaults. Required: url on helmrepository, ocirepository and gitrepository; bucketName and endpoint on bucket; chart, sourceRef.kind and sourceRef.name on helmchart. A url must start with a scheme the CRD’s own pattern admits: http://, https:// or oci:// for helmrepository, and only oci:// under type: oci; oci:// for ocirepository; http://, https:// or ssh:// for gitrepository, so an scp-style git@host:org/repo is refused — write ssh://git@host/org/repo. The CRD has no pattern for endpoint, so it is only required. interval defaults to 60m when unset (a zero duration counts as unset), matching the helmrelease default; a helmrepository with type: oci gets no default, since Flux does not poll it, and its emitted interval reads 0s, the value the Go type always writes. A set interval must be a duration Flux accepts, checked exactly as on helmrelease (go-kure/launcher#601); under type: oci too, where the CRD pattern still applies. A set timeout (a helmchart has none) is checked the same way, against the source CRDs' narrower pattern, which has no h unit: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ (go-kure/launcher#606), so an authored 1h is refused. A timeout of 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). A metav1.Duration would write Duration.String(), 1h30m0s, which the CRD rejects, so such a source is emitted as an unstructured object carrying that text; a timeout below an hour, or none, leaves the source emitted exactly as before. A config built directly gets the same minutes form. An authored verify that names no provider, on an ocirepository or a helmchart, is written with provider: cosign, the API’s default, and an authored provider: "" is refused (go-kure/launcher#790), for the reason given on helmrelease above: 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_OmittedRequiredAndWrittenDefaults holds the five source kinds, with helmrelease and fluxcd-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.kind on helmrelease, refused, and verify.provider on that kind’s chart, on ocirepository and on helmchart, filled. Nothing else is checked or defaulted: enums (type, provider, layerSelector.operation, verify.mode, a HelmChart’s sourceRef.kind and reconcileStrategy) and cross-field rules (a Bucket’s sts against its provider, serviceAccountName against secretRef, a HelmChart’s verify only with a HelmRepository source) are left to the CRD’s own admission. Nor is the source a helmchart names: whether it exists, or is of the kind sourceRef.kind says, is known only on the cluster.

    Policy. ApplyPolicy checks the host the source is fetched from against the policy’s allowed registries (AllowedRegistries), with the same exact match oci, crd and manifests apply: the url host for the three repositories, the endpoint host for bucket. A port is part of the host, so registry.local:5000 matches only an entry with that port. The user of an ssh:// URL is dropped (ssh://git@github.com/org/repo checks github.com); userinfo on an http://, https:// or oci:// 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. A helmchart checks nothing: it fetches from the source its sourceRef names, whose host is checked where that source is authored, as for a helmrelease.

    No refusal prints the value it refused, since a URL or endpoint can carry a credential in its userinfo (the user included), path, query or IPv6 zone (go-kure/launcher#699). An allowlist refusal names the host as displayHost reduces the url’s raw authority (an ssh:// 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, and crd’s and manifests’ http/https check) 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 OCIRepository url, 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 is localhost or contains . or :; anything else is a Docker Hub repository. So oci://ghcr.io and oci://registry/app are 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). An ocirepository url also needs a repository path after the registry; a helmrepository oci:// URL, of either type, may stop at the registry (oci://ghcr.io), since Flux appends the chart name to it.
    • A bucket with provider: gcp is checked against storage.googleapis.com. Flux’s GCP client never reads endpoint; it uses Google Cloud Storage’s own host. The generic, aws and azure providers, and no provider, fetch from endpoint, which stays the host checked, except as the next rule says.
    • A bucket with an Amazon S3 endpoint is refused, unless its provider is azure or gcp. The S3 client behind generic, aws and no provider does not fetch from an Amazon S3 endpoint: it contacts the S3 host of region, which may name any AWS region or partition, or of the bucket’s own location when region is 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: any endpoint containing amazonaws, in any case, counts, because the client’s own host patterns also match look-alikes such as s3.x-amazonaws.com.

    Hosts a source reaches indirectly are not checked: the chart URLs a Helm repository index advertises on other hosts (what passCredentials exists for), a Bucket’s sts.endpoint, the token endpoints a provider authenticates against, and a proxy. Apart from the two bucket cases above, a provider, region or insecure setting 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 under verify and sts, serviceAccountName, a GitRepository include, a HelmChart’s sourceRef — resolves in the namespace the CR lands in, so under a Flux namespace the objects it names must live there. A helmchart whose sourceRef names a source component in the same document finds it there: both move to the Flux namespace. So does the Secret of an external-secret or certificate trait (or the ConfigMap of a configmap trait) on the component when one of the Secret references names it — a certSecretRef naming a certificate trait’s secretName moves that Certificate (FluxNamespaceReads, go-kure/launcher#740); a trait object no reference names stays in the application namespace.

    Generated by the helm rule. An inline source on a helm component generates one of these components: a helmrepository for an http(s):// URL; an ocirepository for an oci:// URL, with a ref.tag from version and a layerSelector copying the Helm chart content layer; a gitrepository for kind: GitRepository with its URL and one source.ref field; or a bucket for kind: Bucket with endpoint and bucketName and no URL. It is named <app>-source-<digest> and shared by every helm component 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 a helmchart.

  • postgresql — provider: cnpg, version (default 16), storageSize (precedence: authored > policy default storageSize > 1Gi), replicas, backup.*, monitoring.enabled, pooler.enabled, poolerName, managedRoles, databases (each with an optional objectName), clusterObjectName, objectStoreObjectName. clusterObjectName names 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, with poolerName, clusterObjectName and objectStoreObjectName added) and emits the CNPG kind components that build the same objects — a cnpg-cluster under the component’s name, a cnpg-objectstore under the same name (one same-name sibling group) when objectStore is set, a cnpg-pooler <name>-pooler when pooler.enabled, and a cnpg-database <name>-<db> per databases entry (a Database given the Pooler’s name, by default the one named pooler beside an enabled pooler, joins its same-name sibling group, as two objects of different kinds). replicas and storageSize are 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 the 1Gi storage 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-protection and force-replace to each member, and fluxcd-patches and fluxcd-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 (a dependency rule or a placement whose 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. A placement of 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 a dependency policy 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>-dependencies and <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 under launcher.gokure.dev/v1alpha1 (go-kure/launcher#281): with both objectStore and pooler, the objects now come in the order Cluster, ObjectStore, Pooler (was Cluster, Pooler, ObjectStore; the objects are the same), and a Database named pooler comes right after the Pooler, ahead of the other Databases. Under a dependency policy 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 in databases is 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), and instances/storage.size in 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 named postgres, template0, template1 or not a DNS-1123 subdomain, and an authored storageSize: "" (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 its cnpg.io/cluster endpoint selector unless the Cluster is named apart (clusterObjectName or the Naming hook, 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 (a poolerName or 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 under launcher.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 authored version: "" is refused by name (version: must not be empty; omit it to default to "16"), with or without imageName: it used to be kept as a value, so without imageName the cluster image was ghcr.io/cloudnative-pg/postgresql: — an empty tag that only the image pull rejected (go-kure/launcher#539). Only an omitted or null version takes 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). Without imageName the image is ghcr.io/cloudnative-pg/postgresql:<version>, so a version that makes a :latest reference, 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 authored imageName is refused under imageName; version is read for the default image only and is not checked beside one. The default version builds. pooler.image is the PgBouncer image, written to the Pooler’s pgbouncer.image and held to the tag rule under its own name (pooler.image: image "…" rejected: …) and, by the cnpg-pooler the pooler lowers to, to the registry allowlist. Like the other pooler settings it needs pooler.enabled. Without it the CloudNativePG operator chooses the PgBouncer image, so under a non-empty allowlist an enabled pooler without pooler.image is 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. Set pooler.image to 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 authored replicas: 0 when the rule lowers it (replicas: must be >= 1, got 0). Behavior-changing under launcher.gokure.dev/v1alpha1: an authored replicas: 0 or a non-positive storageSize used to build and was then refused at apply; it is now refused at build (go-kure/launcher#623). resources forwards every name the shared parser admits — cpu, memory, ephemeral-storage, hugepages-<size> and qualified extended resources such as nvidia.com/gpu, under the same validation as the other seven workload kinds — onto the Cluster’s spec.resources requests/limits unchanged (go-kure/launcher#484; until then any name other than cpu/memory was rejected, so this is an additive change). Only cpu/memory carry a policy default and maximum; CNPG-specific sizing such as ephemeralVolumesSizeLimit is 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 names hugepages-<size> without cpu or memory on either side after policy defaults (resources: hugepages require cpu or memory in requests or limits) — admission would refuse every instance pod. affinity takes the same four keys as the shared affinity property (enablePodAntiAffinity, topologyKey, podAntiAffinityType, nodeSelector) with the same defaults — kubernetes.io/hostname and preferred — but the handler parses it itself into the CNPG AffinityConfiguration instead of calling parseAffinity, because CNPG carries its own affinity shape rather than a corev1.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, and podAntiAffinityType must be preferred or required — an explicit empty string is an error here, not a fallback to the default (go-kure/launcher#448). Rejection reaches the nodeSelector values, 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, rejectUnknownKeys and parseManifestSource. An explicit null affinity is absence — Launcher supplies no explicit affinity settings, exactly as when the property is omitted — matching what pkg/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 primitive authoredValue, 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 empty podAntiAffinityType, remain errors. The shared parseAffinity (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.parameters and externalClusters[].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 as nodeSelector when 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 as type: 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.10 decodes to 1.1). An explicit null value is absence, per “The null contract” below: the key is left out, not refused. The same holds for nodeSelector values in both affinity readers, which share the helper. An objectStore block emits a barman-cloud ObjectStore named after the component (or by objectStoreObjectName or the Naming hook, below) and a WAL-archiver entry on the Cluster for the plugin barman-cloud.cloudnative-pg.io, whose barmanObjectName parameter names that store by the name it took. An authored objectStore.serverName becomes that entry’s serverName parameter, where the plugin reads it (it defaults to the Cluster name); the ObjectStore CRD forbids a configuration.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), and serverName went 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, the backup, monitoring, pooler, bootstrap, replication.synchronous and objectStore blocks and their fields, and each entry of managedRoles, databases (with its extensions), externalClusters and monitoring.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, so pooler.enabled: "true" no longer builds a cluster with no pooler and backup: "s3" no longer builds one with no backup; an explicit null is absence, typed or untyped, so a typed-nil storageSize takes the policy default and a typed-nil pooler.instances or replication.synchronous.number takes the handler default rather than being refused as a bad number. Two further shapes that used to vanish are now errors: a non-string managedRoles[].inRoles entry, and an externalClusters entry without a name. So is a managed role’s connectionLimit: 0 (go-kure/launcher#659), as cnpg-cluster refuses it: CloudNativePG omits a zero connectionLimit from the Cluster and applies its default -1, so the role would deploy with no limit. A role that must not connect sets login: false instead. The lowering repeats the refusal for a PostgresqlConfig built directly rather than parsed. Endpoints runs 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 named postgres) is not seen there: one of the four things an endpoint entry does not see (pkg/oam, “Name roles and the Naming hook”). Two object store paths are required (go-kure/launcher#790): backup.destinationPath where another value of backup is set (retentionPolicy, endpointURL or secretName), and barmanObjectStore.destinationPath of an externalClusters entry that has a barmanObjectStore (backup.destinationPath: required (…), externalClusters[0].barmanObjectStore.destinationPath: required (…)). The Cluster’s CRD requires both. Under a retentionPolicy that is not empty, and in an external cluster’s barmanObjectStore, the lowering used to write "" where the author wrote nothing, which the API server refuses. An endpointURL, a secretName or an empty retentionPolicy without 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 strings cnpg-cluster refuses unauthored; the lowering fills the parent of two more, bootstrap.pg_basebackup.source and the Cluster’s postgresql.synchronous.method (authored here as replication.synchronous.method), only from the string itself, and of the other seven never. TestPostgresqlRule_UnauthoredRequiredStrings reads all eleven in the lowered component. An external cluster’s barmanObjectStore is 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 a PostgresqlConfig built 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 from destinationPath, so endpointURL, secretName or an empty retentionPolicy without one is refused, as a retentionPolicy that 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.recovery and bootstrap.pg_basebackup: the bootstrap is built from a non-empty source, so either block without one is refused (bootstrap.pg_basebackup.source: required (…), or must not be empty (…) for an authored "").
    • replication.synchronous: built from method, so the block without one is refused (replication.synchronous.method: required (any or first)), after a wrongly typed number or dataDurability, which is still refused by its own path.
    • monitoring: built when enabled is true, so customQueries beside an enabled that is not authored is refused (monitoring.enabled: required where monitoring.customQueries is set (…)).
    • pooler: the Pooler is emitted when enabled is true, so instances, type, poolMode, parameters or image beside an enabled that is not authored is refused (pooler.enabled: required where pooler.instances is set (…)).

    An authored enabled: false keeps building, in pooler and in monitoring: enabled is 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. poolerName differs (refused whenever pooler.enabled is 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 empty customQueries list or parameters map only, authors no value and builds as before. TestPostgresqlRule_BlockNotBuilt holds each case. Breaking for a document that authored one of these blocks without its field, among them a backup with an endpointURL or an empty retentionPolicy and no path, a bootstrap.pg_basebackup: {} and a replication.synchronous with a number only. Such a document built, without what the author wrote. An authored pooler.instances is written as authored (go-kure/launcher#790), a 0 or a negative count included. The Pooler CRD gives instances a 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 with pooler.instances: 0 or less: its Pooler now holds that count. A document with instances: 0 got one pod before and gets none now (the postgresql-bootstrap-external fixture 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 the Naming hook” in pkg/oam/README.md): the author’s property, else the consumer’s Naming hook, else the default. poolerName names the Pooler (role pooler, default <name>-pooler) and must be a DNS-1035 label, since CloudNativePG names the pooler’s Service after it; it is refused when pooler.enabled is not true. databases[].objectName names that entry’s Database object (role database, default <name>-<db>) and must be a DNS-1123 subdomain; the entry’s name stays the database created in PostgreSQL. A database whose name cannot be part of an object name (app_data) has no default and needs an objectName: 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 inside Transform: 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. clusterObjectName names the Cluster (role postgresql-cluster) and objectStoreObjectName the ObjectStore (role postgresql-objectstore); the default of each is the component name. The two end in ObjectName as objectName does on a kind component, while poolerName, the older property, keeps the name it has. objectStoreObjectName is refused without objectStore (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’s cluster.name, each Database’s cluster.name, the cnpg.io/cluster endpoint selector, and the barmanObjectName parameter 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 (a clusterObjectName of db-pooler on component db): 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 the postgresql component itself (a poolerName of db on component db, a database’s objectName, 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 the Naming hook”). CloudNativePG derives the Cluster’s Services (<cluster>-rw, <cluster>-ro, <cluster>-r) and Secrets (<cluster>-app and 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 unless objectStore.serverName sets 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 optional oam.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 port 5432) so a downstream platform can synthesize the target-side ingress allow ({comp}-allow-endpoint-ingress) without hardcoding the operator selector. When pooler.enabled is set it declares a second endpoint for the pooler (PgBouncer) pods (cnpg.io/poolerName: <pooler name> on port 5432), 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 authored poolerName, else <component-name>-pooler, and the Cluster name the authored clusterObjectName, else the component name: Transformer.ComponentEndpoints asks no naming hook. The rule also implements oam.NamedEndpointProvider, through which Transformer.ComponentEndpointsNamed resolves 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 its cnpg.io/cluster endpoint 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 by Generate (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 the spec of a postgresql.cnpg.io/v1 Cluster, one schema key per ClusterSpec json field: scalars are typed, and every structured field is an open object or an array of open objects whose description points at the upstream type. ToApplicationConfig decodes the whole property map into cnpgv1.ClusterSpec with builtin.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 from encoding/json itself: 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 empty storage.size, which the decoded spec cannot tell from absence. It carries no launcher opinions: Generate emits 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 from version, primaryUpdateStrategy: unsupervised, enablePDB from the replica count, a 1Gi storage 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 (see postgresql). The one value it writes unasked is instances: 1, the CRD default, because ClusterSpec.Instances has no omitempty and would otherwise serialize as 0. The non-pointer blocks appear even when unauthored, as they do for postgresql: affinity and resources as empty objects, and postgresql as syncReplicaElectionConstraint: {enabled: false}, because that nested field’s enabled has no omitempty. Nulls follow “The null contract” below, and a null is whatever serializes to JSON null: the properties are marshalled with encoding/json and 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 an env entry), 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 as encoding/json writes it — a concrete collection type, a nil behind a pointer, a json.RawMessage and a custom encoder included. A value that does not serialize is refused. An instances below 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. Generate repeats the name and instance-count refusals on what it emits, the count’s upper bound (instances: must be <= 2147483647, got …) included, so a CnpgClusterConfig built in Go without ToApplicationConfig cannot 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 only storage.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 authored 0 or false that 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 and omitempty, so the value is omitted when the Cluster is encoded and the operator applies its CRD default instead. For the linked CNPG version they are managed.roles[].connectionLimit (default -1, unlimited), postgresUID and postgresGID (26), startDelay (3600), stopDelay (1800), switchoverDelay (3600), probes.liveness.isolationCheck.requestTimeout and .connectionTimeout (1000) and replicationSlots.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 as managed.roles[].login: false, superuser: false, minSyncReplicas: 0 or monitoring.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: the signerName and the keyType of 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 as null when they are unauthored, are refused when unauthored: the nodeSelectorTerms of a required node affinity (affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms: required (…)) and the databases of 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_NullRequired derives 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 (microservice or monolith), bootstrap.pg_basebackup.source, externalClusters[].barmanObjectStore.destinationPath, managed.services.additional[].selectorType (rw, r or ro), podSelectorRefs[].name, the name of a postgresql.extensions[] entry and the name and the value of its env[] entries, postgresql.synchronous.method (any or first) and replica.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, so Generate does not repeat it: a CnpgClusterConfig built in Go is not held to it. Nor is the component a postgresql component is lowered to, whose properties are written from a typed spec, so that a string left out there reaches the kind as an authored "": postgresql refuses 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 is instances, which is not refused (below). TestKindComponents_OmittedRequiredAndWrittenDefaults derives 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 own 1), and backup.volumeSnapshot.onlineConfiguration, written {} under an authored volumeSnapshot. The CRD’s default for the object is waitForArchive: true and immediateCheckpoint: false; in a {} it fills waitForArchive with the field’s own default true, and an absent immediateCheckpoint is false. A false or 0 the type keeps (a pointer such as enablePDB: false, a quantity cpu: 0) is emitted as authored. An authored empty string the type omits on a field the CRD defaults (primaryUpdateStrategy: "", logLevel: "") is refused as a defaulted 0 is (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 empty signerName or keyType is refused as an unauthored one is. A match expression authored in one of the spec’s label selectors without its key or its operator is 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. ApplyPolicy enforces the policy postgresql enforces (postgresql lowers onto this kind, so it is the same code), in this order: the instance-count default when instances is not authored (an authored value wins even when it equals the fallback) and its maximum; the cpu and memory request and limit defaults on resources, filling only unset entries, and their maxima; and the storage-size default on storage.size when neither storage.size nor storage.pvcTemplate’s storage request is authored. With no policy default the size is left to the operator; the 1Gi fallback is postgresql’s, applied after the policy by the post-policy step its rule attaches (ApplyPostgresqlDefaults: the fallback is held to the policy maximum, and enablePDB is set from the instance count; a fallback over the maximum is the component’s oam.ViolationError of class oam.RefusalStorageMaximum, as a refusal of ApplyPolicy is). 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, each tablespaces[i].storage (either size or the pvcTemplate request) and ephemeralVolumeSource.volumeClaimTemplate — and the error names the one that exceeds it. Because this kind exposes the two security contexts, it also refuses securityContext.privileged and a securityContext or podSecurityContext windowsOptions.hostProcess unless 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 authored imageName (imageName: image "…" is not from an allowed registry [...]). When imageName is unset, the operator takes the image from the catalog imageCatalogRef names, 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 neither imageName nor imageCatalogRef is 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. Set imageName, or imageCatalogRef to 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. A postgresql component always writes imageName (its derived ghcr.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 each postgresql.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 as imageName is (see Image volumes under pod). An entry without a reference is not checked: the operator takes that image from the catalog imageCatalogRef names (and refuses the Cluster without one), and a catalog’s images are not checked by this kind, as for an unset imageName. 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-imagecatalog and cnpg-clusterimagecatalog hold 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 on manifests, passthrough or a chart, and not of one that already exists in the cluster. A postgresql component writes no extension entry. imageName and 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 :latest imageName or 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 unset imageName and 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; on imageName CloudNativePG’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 refuses hugepages-<size> in resources without cpu or memory after 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. Endpoints declares the same primary endpoint as postgresql (cnpg.io/cluster: <Cluster name> on port 5432), and only for a component the parse accepts: what ToApplicationConfig refuses of the name or of the properties is refused where endpoints are collected, in the same words (instances: must be >= 1, got 0). What Generate refuses once the policy is applied is not repeated there. TestCnpgClusterSchema_CoversClusterSpec pins the schema to ClusterSpec by reflection: each json field is published with its type or listed with a reason in cnpgClusterExcludedFields (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 Pooler and Database and a Barman Cloud plugin ObjectStore (barmancloud.cnpg.io/v1), built on the cnpg-cluster recipe: one schema key per json field of PoolerSpec, DatabaseSpec or ObjectStoreSpec, the whole property map decoded strictly into that type under the same null contract, the same refusal of an authored 0 or false the type cannot carry (for the linked versions only instanceSidecarConfiguration.retentionPolicyIntervalSeconds, default 1800, 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. Generate emits 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.name and pgbouncer on the Pooler (pgbouncer: {} selects PgBouncer’s defaults); cluster.name, name and owner on the Database; configuration.destinationPath on the ObjectStore. The published schema marks the top-level property of each as required (go-kure/launcher#790): cluster and pgbouncer of a cnpg-pooler; cluster, name and owner of a cnpg-database; configuration of a cnpg-objectstore. That refuses no component that built before, since the kind refused each without it already. A cluster.name must 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 of template.spec without its key or its operator is refused too, by path; an authored empty one is not (presence only; see “A label selector’s match expressions”). cnpg-pooler writes 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). ApplyPolicy applies the workload kinds’ pod gates to template.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 in template.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 authored pgbouncer.image. With or without a policy, when the component is read and again at generation, an authored pgbouncer.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 :latest one 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 named pgbouncer, from pgbouncer.image, from the catalog pgbouncer.imageCatalogRef names (not checked by this kind, as for cnpg-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), as cnpg-cluster does on its resources; the other resource-name rules of the shared parser are left to the API server. A template declaring ephemeralContainers, activeDeadlineSeconds, priority or overhead is 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 under template.spec, which the pod spec’s type leaves out when they are empty, are refused when unauthored or empty: the action of a restart rule of a container or an init container and the operator of its exit codes (template.spec.containers[0].restartPolicyRules[0].action: required (…)), and the signerName and keyType of 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_OmittedRequiredAndWrittenDefaults derives the fields from the CRD and shows the refusals. Five more that the CRD requires under template.spec, which the pod spec’s type writes as null when they are unauthored, are refused when unauthored: the nodeSelectorTerms of a required node affinity, the priority of an eviction responder, the monitors of a cephfs or an rbd volume, and the secretRef of a scaleIO volume (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.containers is such a field too and is not refused: the kind writes containers: [] itself (below). TestKindComponents_NullRequired derives these fields and pgbouncer from 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 authored pgbouncer.imageCatalogRef: its key (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, so Generate does not repeat it: a CnpgPoolerConfig built 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 in template.spec: resources cpu limit "2" exceeds enforced maximum "1". A template that lists no containers (labels only, say) is written with containers: []: the Go type cannot omit the template’s spec, the CRD requires containers once it is present, and the operator adds its pgbouncer container either way. Endpoints declares the PgBouncer pods (cnpg.io/poolerName: <Pooler object name> on port 5432: the component name unless objectName or the naming hook names the Pooler), byte-identical to postgresql’s pooler endpoint for a pooler of that name (<component-name>-pooler by default, of the postgresql component), and only for a component the parse accepts: what ToApplicationConfig refuses is refused where endpoints are collected, in the same words (pgbouncer: required (an empty object selects PgBouncer's defaults)). cnpg-database also refuses the names the CRD reserves (postgres, template0, template1). The ensure of each schema, extension, fdw and server has no omitempty but a CRD default of present, so Generate writes present where it is unauthored, as the API server would have; a test derives that list from the CRD too. An authored ensure: "" there is refused by its path (schemas[0].ensure: "" is refused by the Database CRD, …) rather than written as present, 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-objectstore caps 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 refuses configuration.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 exposes ComponentName() string (the oam.ComponentNamed interface) so consumers can attribute the emitted resource to its owning OAM component. object must be a single object; a list is rejected. Generate emits the map verbatim as one resource and fills in metadata it finds missing: metadata.name defaults to the component name when it is absent, empty, or not a string, and on a namespaced object metadata.namespace likewise — 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 manifests gives a scopeOverrides entry, with clusterScoped in 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. A PriorityClass, a ClusterRole or a Namespace gets no namespace without the property. A clusterScoped that contradicts the table is refused: true on a namespaced built-in, an explicit false on 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 from manifests, which ignores a scopeOverrides entry for such a kind: its list may name kinds the source does not hold, while clusterScoped is 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 clusterScoped decides, true or false, also against the table. A custom resource kure registers as cluster-scoped that the target cluster serves namespaced takes clusterScoped: false.
    • Not authored (absent or null): the table decides for a kind kure registers, so a cert-manager ClusterIssuer gets no namespace. A kind nothing knows is treated as namespaced, the default this component always had. manifests fails closed on an unknown scope; passthrough does not, since every custom resource passed through without the property relies on that default.

    No CustomResourceDefinition is 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-string namespace on a cluster-scoped object is neither rejected nor defaulted and reaches the output as authored. The resolution runs in Generate too, on the object about to be emitted, so a PassthroughConfig built directly is held to it; its ClusterScoped field states true or nothing, since an explicit false exists 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 clusterScoped was not set; clusterScoped: true on a built-in namespaced kind and clusterScoped: false on 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. Generate emits the map as one resource, so a list would arrive downstream as one named envelope whose items never 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 own Unstructured.IsList — items present and a sequence — never the kind name, so a typed ConfigMapList is caught and a CRD whose kind merely ends in List with no items still compiles.

    This arm is deliberately stricter than Kustomize for a kind that does not end in List but carries a top-level items array, such as {kind: Widget, items: [...]}. Kustomize’s build keeps that as one resource, because it consults items only on a List-suffixed kind. Flux’s kustomize-controller then decodes the build output with the same IsList predicate and no kind check, and applies the array’s members instead of the object: a Widget whose items holds a ConfigMap puts that ConfigMap on the cluster, in whatever namespace it names, and never applies the Widget. With scalar members the Flux apply fails outright. Either way it is not one object, so it is rejected.

    A second arm catches what IsList structurally cannot. It requires items to be exactly a []interface{}, so an authored items: 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 null items on a kind ending in List rather than a present one — a CRD may legitimately carry an object-valued items field that must keep compiling, and an ordinary, non-List kind may equally carry a null-valued items field (a plausible spec-field collision) without being an envelope at all. Matches Kustomize’s own inlineAnyEmbeddedLists, which checks the kind suffix before ever consulting items; Flux agrees, since a null items is not a list to IsList and 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]any and []any, but a Go-assembled body can hold a map[string]string or 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 *Location inside a time.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. Object is 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 runs ToApplicationConfig at all — three routes reaching Generate with a body the arms never saw. Generate therefore re-runs, on the map it is about to emit, every check that must hold of an emitted body: a non-empty apiVersion and kind, 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, Generate checking only list shape — a PassthroughConfig holding an empty map passed both: non-nil, so it cleared the no-object guard, and carrying no items, so it cleared both arms, leaving Generate to emit a document consisting of nothing but the metadata it had just stamped on.

    Policy. ApplyPolicy holds 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 with NoopPolicy when 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, and ValidateImageRef (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 :latest one 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 the maxReplicas of a HorizontalPodAutoscaler, in any API version, to the replica maximum (MaxReplicas). A PersistentVolume is held to what the persistentvolume kind holds its own to: a hostPath or local source needs AllowHostPathVolumes(), and spec.capacity.storage is 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 the pkg/oam README), as the secret trait 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 answers false: a passthrough Secret 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 and manifests refuse it (helmtemplate’s Undeclared fields): when the component is built and again by Generate, 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’s json.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 of manifests has 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. Generate runs the check again on the object it is about to emit, once ApplyPolicy has supplied a policy, because Object is 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 whose maxReplicas is 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: a CiliumNetworkPolicy or CiliumClusterwideNetworkPolicy whose icmps field leaves its type out, 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 MetalLB BGPPeer at metallb.io/v1beta2 is 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 states Kind or apiversion beside 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: the Kind case was read as its Go type and checked, and so was the apiversion case where that key states a version the kind is registered in (apiversion: v1 beside apiVersion: v1); with another version there it was unreadable before, for another cause. Every build decodes the object, since the transform applies NoopPolicy when 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 passthrough object 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 :latest image, or a value over a maximum — and with no policy passed the first three are always refused, since NoopPolicy denies them. A consumer allows what it wants allowed through its policy, not per component: AllowPrivileged(), AllowHostNetwork(), AllowHostPID(), AllowHostIPC() and AllowHostPathVolumes() for the security flags, the registry in AllowedRegistries() (or no allowlist), the capability lists, and the maxima (MaxReplicas(), MaxCPU(), MaxMemory(), MaxStorageSize()). An untagged or :latest image 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 Secret holds (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 direct ApplyPolicy(nil), or Generate on a config no policy was applied to) checks nothing, and ApplyPolicy(nil) withdraws no policy applied before.

  • crd / manifests — inline xor url; manifests adds scopeOverrides (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 a CustomResourceDefinition bundled 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’s stampManifestNamespaces, an error naming both the override’s and the CRD’s declared scope; the resolution itself is resolveObjectScope, which the helmtemplate component’s scopeOverrides goes 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: a CustomResourceDefinition document; any kind whose kure scope-table entry comes from ScopeSourceBuiltin — i.e. from the generated upstream types, PriorityClass, APIService and 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 in go.mod. Such a kind is detected by asking manifest.Scope itself 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 no metadata.namespace. Conversely, a cluster-scoped object (either component, any scope source) that authors metadata.namespace is 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 decode helmtemplate gives 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 a CustomResourceDefinition kept so still passes crd and 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’s items schema 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 MetalLB BGPPeer at metallb.io/v1beta2 is 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 (9007199254740993 as 9007199254740993; 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 manifests or crd source yields is checked against the environment policy with the check template delivery (helmtemplate) and passthrough use: 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, ValidateImageRef on 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’s maxReplicas to the replica maximum. A PersistentVolume is held to what the persistentvolume kind holds its own to: a hostPath or local source needs AllowHostPathVolumes(), and spec.capacity.storage is 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": …). A crd source holds only CustomResourceDefinitions, none of which the check reads, so crd builds as before.

    A core Secret a manifests source yields, inline or fetched, is refused under a policy that forbids explicit secrets (oam.ExplicitSecretPolicy), as the secret trait and a passthrough Secret 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 answers false. Not covered: a Secret a chart renders under template delivery (helmtemplate, helm with delivery: 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 inline source is checked by ApplyPolicy, the transform’s policy step, and the refusal is that step’s *oam.ViolationError. A url source is checked in Generate, on the objects it is about to emit: ApplyPolicy checks 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 a url source. A refusal in Generate is the same *oam.ViolationError, naming the component; a fetch or parse failure there keeps its own error and is not a policy violation. Generate also checks an inline source 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 …): a v1 List, a typed list (DeploymentList) and, since go-kure/kure#1014, a list of a kind the scheme does not register (a kind ending in List that states items), 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 one helmtemplate’s Rendered objects describes, with the same errors under manifest source: parse manifests: …: a label or an annotation on a list’s own metadata, a Kind, apiversion or Items key, a null item, and an object of a kind that is no list with a top-level items array, which is refused with a policy or with none and is no policy violation. Breaking (go-kure/launcher#790): the table under helmtemplate lists what built before and is refused now.

    Behaviour change: before go-kure/launcher#794 the objects of a manifests source 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 :latest image, 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, since NoopPolicy denies them. A consumer allows what it wants allowed through its policy, not per component, exactly as for passthrough. An untagged or :latest image 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 Secret holds (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 direct ApplyPolicy(nil), or Generate on a config no policy was applied to) checks nothing, and ApplyPolicy(nil) withdraws no policy applied before.

    A url that 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’s displayURL; 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 as dial failed: connection refused; anything else reads request failed or response 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.Is and errors.As still find it, so a timeout matches context.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.

PropertyTypeEffectKind
podManagementPolicyenumOrderedReady (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}}objecttype 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
revisionHistoryLimitint ≥ 0Retained controller revisions.additive
minReadySecondsint ≥ 0Readiness settling time before a pod counts as available.additive
persistentVolumeClaimRetentionPolicy{whenDeleted, whenScaled}objectEach is Retain or Delete.additive
ordinals{start}objectstart shifts the replica ordinal range and is required once ordinals is authored; >= 0.additive
selector (claim)objectmatchLabels/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)objectstorage 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)enumFilesystem 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)stringWhere 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)objectMirrors 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)stringDNS-1123 subdomain naming a VolumeAttributesClass.additive
storageClass (claim)stringBehavior-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 capabilitystringPre-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)quantityBehavior-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 typelistBehavior-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) — collisionsstringBehavior-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 typestringBehavior-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 on deployment. 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 as mountPath (or, for a volumeMode: Block claim, devicePath) on the same entry.
  • dataSource (claim) — when dataSourceRef carries no namespace the apiserver mirrors it into the superseded dataSource field, so authoring both is redundant; when it does carry a namespace the apiserver does not mirror, and dataSource must stay empty. Either way the field is authored through dataSourceRef alone.

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, or Transform without 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, which kurel build calls). 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 through UnsupportedFieldHint (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:

PositionOnKeys
a container’s resources: the main container’s, and an initContainers or sidecars entry’severy workload kindclaims
the pod’s podResourcesevery workload kindclaims (upstream validation forbids claims at pod level: “claims may not be set for Resources at pod-level”)
an initContainers entryevery workload kindprobes, lifecycle (see Container fields)
the affinity shorthandwebservice, workernodeAffinity, podAffinity, podAntiAffinity
a volumeClaimTemplates entrystatefulsetvolumeName, 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

KeyOnWhy
selectordeployment, statefulset, daemonset (webservice, worker)Builder-managed (app: <component>), equal to the template labels, immutable once created.
selector, manualSelectorjob, cronjobThe job controller generates the selector from a per-job label; a hand-written one adopts another job’s pods.
schedulingjob, cronjobAlpha 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, serviceAccountevery workload kindSee Pod-level properties.
evictionRespondersevery workload kindAlpha upstream, behind the EvictionRequestAPI feature gate.
restartPolicyRules (the main container’s)every workload kindUpstream 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, clusterIPsserviceSee the service kind.
dataSourcepersistentvolumeclaim, the pvc traitAuthored through dataSourceRef.
secretsserviceaccountThe 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.claimsa container’s resources, on every kind that reads themAn 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.claimsevery workload kindUpstream 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 fieldOnAuthored as
the pod templatethe five pod-building kindsThe 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.
jobTemplatecronjobThe component’s own job-level, pod-level and container properties.
containersthe five pod-building kindsThe main container is the component’s container properties; further ones are sidecars entries on the kinds that have them.
the main container’s namethe five pod-building kindsNot authored: the container is named after the component.
livenessProbe, readinessProbe, startupProbethe five pod-building kindsprobes.liveness, probes.readiness, probes.startup.
volumeMounts, volumeDevicesthe five pod-building kindsA volumes entry’s mountPath, or its devicePath for a volumeMode: Block claim.
resources (the claim’s)persistentvolumeclaim, the pvc traitsize, 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 the ContainerStopSignals feature gate, alpha and off by default through Kubernetes 1.37, and holds the signal to the pod’s os.name. A container’s lifecycle reads postStart and preStop.
  • ports[].hostPort and ports[].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. A ports entry reads containerPort, name and protocol.

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: service and persistentvolumeclaim on every key they refuse, and deployment on the pod-level and container-level keys (not on selector and template).
  • Refused, as any value is: statefulset, daemonset, job, cronjob, serviceaccount, and webservice and worker; and every kind on resources.claims, on podResources.claims and on the keys refused under the affinity shorthand.

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_CoverEveryUpstreamField walks 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 later k8s.io/api adds 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’s resources and the pod’s podResources.
  • TestRefusedKeys_BothPathsGiveTheReason holds every entry of every refusal map to the two texts above; TestRefusedKeys_OneLevelDown holds the two refusals inside a declared property.
  • TestHandParsedKinds_MatchExpressionsAtEverySelector runs 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_TemplateMetadataAndContainerName and TestHandParsedKinds_ServicePortFieldsReachTheObject pin 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 Items type, 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: ValidateAuthoredProperties checks 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#394 and go-kure/launcher#570 converted the parsers they covered; the last of them are listed with the null row of the deployment table above.

  • An empty object is not a null and is not absent. {} and an absent key mean different things to Kubernetes wherever a LabelSelector is 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 single corev1.PodAffinityTerm disagree, in upstream’s own words:

    fieldnull{}
    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 labelSelector and is wrong for namespaceSelector, 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.

- name: web
  type: deployment
  properties:
    image: ghcr.io/example/web:1.0.0
    labels:
      team: payments
    annotations:
      example.com/owner: "payments@example.com"

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 app label 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 set app select 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 key or its operator (selector.matchExpressions[0].operator: required (the operator of the expression: In, NotIn, Exists or DoesNotExist)). The object would carry key: "" or operator: "" and not show the omission.
  • An operator that is none of the four (selector.matchExpressions[0].operator: "Equals" is not a label selector operator (In, NotIn, Exists or DoesNotExist)).
  • An operator and values that do not go together: In or NotIn without a value (selector.matchExpressions[0].values: required with the operator In (at least one value)), Exists or DoesNotExist with 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:

KindSelectors
networkpolicypodSelector; the podSelector and namespaceSelector of every ingress[].from[] and egress[].to[] peer
poddisruptionbudgetselector
clusterroleevery entry of aggregationRule.clusterRoleSelectors
validatingwebhookconfiguration, mutatingwebhookconfigurationthe namespaceSelector and objectSelector of every webhook
pod, and under template.spec podtemplate, replicaset and replicationcontrollerthe 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.

KindSelectorsGround
servicemonitor, podmonitorselectorthe linked type, by the generator’s rule (below)
prometheus-probetargets.ingress.selectorthe same
alertmanagerthe selectors a pod holds, at the top of the spec; the selector of the claim template of storage.volumeClaimTemplate and of storage.ephemeral; alertmanagerConfigSelector; alertmanagerConfigNamespaceSelectorthe same
thanosrulerthe selectors a pod holds, at the top of the spec; the selector of the claim template of storage.volumeClaimTemplate and of storage.ephemeral; ruleSelector; ruleNamespaceSelectorthe same
prometheusthe 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-clusterthe 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[].selectorthe Cluster CRD of the linked module
cnpg-poolerunder template.spec, the selectors a pod holdsthe Pooler CRD of the linked module
issuer, clusterissuerthe labelSelector and namespaceSelector of every pod affinity and anti-affinity term of an HTTP01 solver’s podTemplate.spec.affinity, under ingress and under gatewayHTTPRoutethe Issuer and ClusterIssuer CRDs of the linked module
gateway, listenerseta listener’s allowedRoutes.namespaces.selector; a Gateway’s allowedListeners.namespaces.selectorthe CRDs of both Gateway API channels
cilium-nodeconfignodeSelectorthe CiliumNodeConfig CRD of the linked module
secretstore, clustersecretstoreconditions[].namespaceSelectorthe linked type, by the generator’s rule (below)
clusterexternalsecretnamespaceSelector; every entry of namespaceSelectorsthe same
imageupdateautomationpolicySelectorthe same
resourcesetinputproviderevery entry of selectorsthe ResourceSetInputProvider CRD of the linked module, and the linked type, by the generator’s rule
replicationsource, replicationdestinationthe 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 syncthingthe 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:

FamilyOwner typeTypical fields
AFFthe core/v1 affinity terms: NodeSelectorRequirement, PodAffinityTerm, PreferredSchedulingTerm, WeightedPodAffinityTerma node selector requirement’s key and operator; a pod affinity term’s topologyKey; a weighted or preferred term’s weight, podAffinityTerm and preference
SECSeccompProfile, AppArmorProfile, Sysctla profile’s type; a sysctl’s name and value
PODany other core/v1 typethe key of a secret or config map key reference; a container’s name; an imageCatalogRef’s kind and name
KINDa type of the kind’s own API, or of metav1 other than a label selector’s match expressionan 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.

KindNot refusedAFFSECPODKIND
issuer, clusterissuer42 each366
replicationsource113721625
replicationdestination85541219
cnpg-cluster1571875280
cnpg-pooler1781881475
cnpg-database1111
cnpg-objectstore29920
httproute4444
grpcroute3434
servicemonitor, podmonitor, prometheus-probe19 each19
alertmanager2411882078
helmrelease1717
fluxcd-kustomization1414
bucket, ocirepository7 each7
gitrepository66
helmchart, helmrepository3 each3

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.