What happened?
function-extra-resources doesn't infer the XR's namespace when a Reference-typed extra resource omits ref.namespace. The function forwards ref.namespace to the emitted ResourceSelector verbatim — empty string in, empty string out — which means the downstream fetcher does a Get(name, namespace="") against the apiserver. For namespaced kinds (the common case for ConfigMap, Secret, etc.) that's a malformed request, and the fetcher returns NotFound. The pipeline step then FATALs with Required extra resource "<name>" not found.
The user reading the field name ref: reasonably expects it to behave like every other reference shape in core Kubernetes and the Crossplane ecosystem — when the holding object is namespaced and the referenced kind is namespaced, an omitted namespace should default to the holding object's namespace. Examples of that convention:
| Reference |
Behavior on omitted namespace |
corev1.ConfigMapKeyRef / SecretKeyRef (Pod env) |
No namespace field exists at all — always Pod's namespace |
corev1.PersistentVolumeClaimVolumeSource |
Same as Pod |
metav1.OwnerReference |
No namespace field — same as owned (or owner is cluster-scoped) |
Ingress Backend.Service |
Same as Ingress |
RoleBinding.Subjects[*].ServiceAccount |
Defaults to RoleBinding's namespace |
NetworkPolicy.podSelector |
Same namespace as the NetworkPolicy |
Crossplane Composition.compositeTypeRef |
XRD is cluster-scoped, no namespace concern |
Crossplane crossplane-runtime Local/CrossplaneRef helpers |
Default to same namespace as the holding object |
function-extra-resources is the outlier — ref.namespace is treated as a static string with no defaulting, so the only working configuration for a namespaced kind is to hardcode a literal namespace (which doesn't work when the same composition is used across XRs in different namespaces).
The bug surfaces clearly in crossplane internal render, which has run the same FetchingFunctionRunner → ExistingRequiredResourcesFetcher → client.Get(name, ns) chain since the subcommand was introduced in Crossplane v2.3.0 (PR crossplane/crossplane#7339, merged 2026-05-04). Production reconciles use the same chain, so the failure mode applies to live clusters too — crossplane internal render just makes it easy to reproduce out-of-cluster.
Proposed fix
In buildRequirements (fn.go:119), default Namespace from the XR when the user yaml omits it AND the referenced kind is namespaced:
case v1beta1.ResourceSourceTypeReference, "":
ns := extraResource.Namespace
if ns == "" {
// Match the convention used by every other namespaced
// reference in Kubernetes and Crossplane: when the user
// doesn't say where to look, look in the same namespace
// as the holding object.
if scoped, err := isNamespacedKind(extraResource.APIVersion, extraResource.Kind); err == nil && scoped {
ns = xr.Resource.GetNamespace()
}
}
extraResources[name] = &fnv1.ResourceSelector{
ApiVersion: extraResource.APIVersion,
Kind: extraResource.Kind,
Match: &fnv1.ResourceSelector_MatchName{MatchName: extraResource.Ref.Name},
Namespace: ns,
}
Determining "is this kind namespaced" needs either runtime discovery or a static hint. Two paths:
- Static hint on the input — add an optional
scope: Cluster | Namespaced (default Namespaced) so the user disambiguates. Matches the pattern of other contrib functions (function-go-templating, function-kcl, etc.), which are designed to be apiserver-free and declare needs via Requirements rather than fetching directly.
- Discovery via a kube client — wire up an apiserver client + cached discovery REST mapper inside the function.
function-sdk-go does not expose one, so this would mean a new dep on apiserver access from the function pod (RBAC, kubeconfig boot, cache lifecycle) and would break crossplane internal render flows that lack apiserver access.
Behaviorally (with the static-hint approach):
- Existing manifests that do set
namespace are unchanged.
- Existing manifests that don't set
namespace and target a namespaced kind: broken in v2.3+ (and would fail in any production reconciler running this code path); now work.
- Existing manifests that don't set
namespace and target a cluster-scoped kind: must add scope: Cluster to opt out of the new defaulting. (Worked incidentally before because the function emitted an empty namespace verbatim.)
I'm happy to send the PR if you'd like.
How can we reproduce it?
Two XRs in different namespaces, each consuming a same-named ConfigMap from their own namespace via a single shared composition:
XRD (any namespaced XR works)
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: xnopresources.collision.example.org
spec:
scope: Namespaced
group: collision.example.org
names:
kind: XNopResource
plural: xnopresources
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
value: { type: string }
Composition (note the ref: block has no namespace)
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xnopresources.collision.example.org
spec:
compositeTypeRef:
apiVersion: collision.example.org/v1alpha1
kind: XNopResource
mode: Pipeline
pipeline:
- step: fetch-external-resources
functionRef:
name: function-extra-resources
input:
apiVersion: extra-resources.fn.crossplane.io/v1beta1
kind: Input
spec:
extraResources:
- apiVersion: v1
kind: ConfigMap
into: nsConfig
type: Reference
ref:
name: collision-config
- step: ready
functionRef: { name: function-auto-ready }
Resources in the cluster
apiVersion: v1
kind: Namespace
metadata: { name: ns-a }
---
apiVersion: v1
kind: Namespace
metadata: { name: ns-b }
---
apiVersion: v1
kind: ConfigMap
metadata: { name: collision-config, namespace: ns-a }
data: { value: from-ns-a }
---
apiVersion: v1
kind: ConfigMap
metadata: { name: collision-config, namespace: ns-b }
data: { value: from-ns-b }
---
apiVersion: collision.example.org/v1alpha1
kind: XNopResource
metadata: { name: xr-in-ns-a, namespace: ns-a }
spec: { value: hi }
---
apiVersion: collision.example.org/v1alpha1
kind: XNopResource
metadata: { name: xr-in-ns-b, namespace: ns-b }
spec: { value: hi }
Expected: each XR's pipeline sees its own namespace's collision-config. Reconciliation succeeds, downstream resources reflect each namespace's data.value.
Actual: both XRs FATAL with pipeline step "fetch-external-resources" returned a fatal result: verifying and sorting extra resources: Required extra resource "nsConfig" not found. The function emits ResourceSelector{MatchName: "collision-config", Namespace: ""}, the fetcher's client.Get(NamespacedName{Namespace: "", Name: "collision-config"}) for a namespaced kind doesn't match either ConfigMap.
The same composition works if the user adds an explicit ref.namespace: ns-a (and a separate composition for ns-b). That defeats the point of a single composition serving multiple namespaces.
This was originally surfaced in crossplane-contrib/crossplane-diff as TestCompDiffIntegration/CrossNamespaceResourceCollision. The same composition would fail identically in a live cluster — the bug isn't render-tooling-specific.
What environment did it happen in?
Function version: v0.2.0 (also confirmed against v0.3.0 source — same code path). The behavior has been the same since at least v0.2.0.
- Crossplane:
v2.3.0 and onward (PR crossplane/crossplane#7339 introduced the crossplane internal render subcommand and the current FetchingFunctionRunner → ExistingRequiredResourcesFetcher chain in v2.3.0; production reconciles use the same chain so the bug applies to live clusters since v2.3.0 too)
- Kubernetes: any (the apiserver-side behavior —
Get with empty namespace on a namespaced kind — is universal)
(issue created with 🤖 from a conversation w/@jcogilvie in crossplane-diff where I experienced the bug failing a test after switching to internal render)
What happened?
function-extra-resourcesdoesn't infer the XR's namespace when aReference-typed extra resource omitsref.namespace. The function forwardsref.namespaceto the emittedResourceSelectorverbatim — empty string in, empty string out — which means the downstream fetcher does aGet(name, namespace="")against the apiserver. For namespaced kinds (the common case forConfigMap,Secret, etc.) that's a malformed request, and the fetcher returns NotFound. The pipeline step then FATALs withRequired extra resource "<name>" not found.The user reading the field name
ref:reasonably expects it to behave like every other reference shape in core Kubernetes and the Crossplane ecosystem — when the holding object is namespaced and the referenced kind is namespaced, an omittednamespaceshould default to the holding object's namespace. Examples of that convention:namespacecorev1.ConfigMapKeyRef/SecretKeyRef(Pod env)namespacefield exists at all — always Pod's namespacecorev1.PersistentVolumeClaimVolumeSourcemetav1.OwnerReferencenamespacefield — same as owned (or owner is cluster-scoped)Backend.ServiceRoleBinding.Subjects[*].ServiceAccountNetworkPolicy.podSelectorComposition.compositeTypeRefcrossplane-runtimeLocal/CrossplaneRef helpersfunction-extra-resourcesis the outlier —ref.namespaceis treated as a static string with no defaulting, so the only working configuration for a namespaced kind is to hardcode a literal namespace (which doesn't work when the same composition is used across XRs in different namespaces).The bug surfaces clearly in
crossplane internal render, which has run the sameFetchingFunctionRunner→ExistingRequiredResourcesFetcher→client.Get(name, ns)chain since the subcommand was introduced in Crossplane v2.3.0 (PR crossplane/crossplane#7339, merged 2026-05-04). Production reconciles use the same chain, so the failure mode applies to live clusters too —crossplane internal renderjust makes it easy to reproduce out-of-cluster.Proposed fix
In
buildRequirements(fn.go:119), defaultNamespacefrom the XR when the user yaml omits it AND the referenced kind is namespaced:Determining "is this kind namespaced" needs either runtime discovery or a static hint. Two paths:
scope: Cluster | Namespaced(defaultNamespaced) so the user disambiguates. Matches the pattern of other contrib functions (function-go-templating,function-kcl, etc.), which are designed to be apiserver-free and declare needs viaRequirementsrather than fetching directly.function-sdk-godoes not expose one, so this would mean a new dep on apiserver access from the function pod (RBAC, kubeconfig boot, cache lifecycle) and would breakcrossplane internal renderflows that lack apiserver access.Behaviorally (with the static-hint approach):
namespaceare unchanged.namespaceand target a namespaced kind: broken in v2.3+ (and would fail in any production reconciler running this code path); now work.namespaceand target a cluster-scoped kind: must addscope: Clusterto opt out of the new defaulting. (Worked incidentally before because the function emitted an empty namespace verbatim.)I'm happy to send the PR if you'd like.
How can we reproduce it?
Two XRs in different namespaces, each consuming a same-named ConfigMap from their own namespace via a single shared composition:
XRD (any namespaced XR works)
Composition (note the
ref:block has nonamespace)Resources in the cluster
Expected: each XR's pipeline sees its own namespace's
collision-config. Reconciliation succeeds, downstream resources reflect each namespace'sdata.value.Actual: both XRs FATAL with
pipeline step "fetch-external-resources" returned a fatal result: verifying and sorting extra resources: Required extra resource "nsConfig" not found. The function emitsResourceSelector{MatchName: "collision-config", Namespace: ""}, the fetcher'sclient.Get(NamespacedName{Namespace: "", Name: "collision-config"})for a namespaced kind doesn't match either ConfigMap.The same composition works if the user adds an explicit
ref.namespace: ns-a(and a separate composition forns-b). That defeats the point of a single composition serving multiple namespaces.This was originally surfaced in
crossplane-contrib/crossplane-diffasTestCompDiffIntegration/CrossNamespaceResourceCollision. The same composition would fail identically in a live cluster — the bug isn't render-tooling-specific.What environment did it happen in?
Function version:
v0.2.0(also confirmed againstv0.3.0source — same code path). The behavior has been the same since at leastv0.2.0.v2.3.0and onward (PR crossplane/crossplane#7339 introduced thecrossplane internal rendersubcommand and the currentFetchingFunctionRunner→ExistingRequiredResourcesFetcherchain in v2.3.0; production reconciles use the same chain so the bug applies to live clusters since v2.3.0 too)Getwith empty namespace on a namespaced kind — is universal)(issue created with 🤖 from a conversation w/@jcogilvie in crossplane-diff where I experienced the bug failing a test after switching to
internal render)